πŸ“¦kthx

  • kthx is quick sites: pick a name, drop a zip or an index.html, and it answers at https://<name>.kthx.dev β€” with a database, a socket, and a visitor identity of its own. The sites are public; claiming one is not: the control API answers on kthx.${SECRET_DOMAIN}, a name on the gateway's private address that the lab, the offsite LAN and the tailnet reach and the tunnel never carries. One Bun process on the offsite cluster serves the whole zone and that name. It is a peer of Architecture/Spindrift, not a part of it: it shares the gateway that fronts it and nothing else.

  • Where it lives

    • apps/kthx/server/ is the process. index.ts dispatches on Host β€” the apex, a site host, or nothing β€” and everything else hangs off that: serve.ts answers static bytes, sites.ts is claim/release/serve/hold/delete, documents.ts and pg.ts are the per-site database, realtime.ts is the socket, releases.ts owns the unpack and the volume, depot.ts reads and writes the bucket, me.ts signs the visitor cookie, limits.ts holds the buckets and caps. Its own schema is numbered SQL in apps/kthx/server/migrations/, applied at boot.

    • apps/kthx/cli/ is the kthx command: deploy claims a name when a directory has none and uploads, dev serves a directory locally and proxies /api/* and /files/* to the site host so the page runs against its real database, rollback and release drive the hold latch. The name is kthx.json in the directory; the bearer is in $XDG_CONFIG_HOME/kthx/sites.json, never in the directory that gets uploaded. It ships as a tarball the apex serves at /cli/kthx.tgz β€” bun add -g https://kthx.dev/cli/kthx.tgz β€” built by the cli stage of apps/kthx/Dockerfile. There is no npm package, so kthx upgrade is what replaces an installed copy: it re-runs bun add -g on that tarball. The build id apps/kthx/pack.ts writes into dist/version.json travels both inside the tarball and beside it in the image, which is what lets an installed copy compare itself against x-kthx-build on one capped HEAD a day and say update available.

    • packages/kthx/ is what a browser gets: sdk.js is window.kthx, landing.html is the apex page, skill.md is the agent reference the apex serves, and favicon.ts is the dot a site with no icon of its own answers with.

    • packages/archive/ holds the tar, zip and GCS code both apps/kthx and apps/spindrift import β€” one reader for an uploaded archive, one address parser for a gs:// location.

    • packages/charts/kthx/ and clusters/offsite/apps/kthx/ are the workload: a Deployment, its Service and ServiceAccount, the sites volume, a CloudNativePG Cluster, a nightly dump, two network policies, and the HTTPRoute. clusters/offsite/monitoring/kthx-rules.yaml is what pages about it. The zone and the bucket are terraform/network/cloudflare/kthx.dev.tf and terraform/gcp/projects/bluenose/.

  • How a name resolves

    • Cloudflare holds the zone and terminates TLS. The apex and a wildcard both CNAME to the spindrift-control tunnel, which reaches cilium-gateway-spindrift-apps on plain HTTP β€” so the zone's names have no TLS listener in the cluster; the private host below is the one kthx name that has one. The zone's cache rules bypass the edge for /api/ and /files/ and cache everything else on origin terms β€” and a release file's terms are no-cache with the digest etag, so the bytes are cached everywhere, every read revalidates, and a new release is the next refresh rather than a minute later.

    • packages/charts/kthx/templates/httproute.yaml carries kthx.dev and *.kthx.dev to the Service. The route is in the kthx namespace and the gateway is not, so that namespace wears app.kubernetes.io/part-of: spindrift β€” the label the listener admits routes by. Both external-dns hold-out annotations are on the route: the records belong to Terraform and point at the tunnel, not at the gateway's private address.

    • The same template renders a second route for the private host, control.host in clusters/offsite/apps/kthx/helm-release.yaml, pinned by sectionName to the gateway's *.${SECRET_DOMAIN} TLS listener and carrying no hold-out: external-dns publishes that one name at the gateway's private address, which is the whole of how it resolves. The tunnel reaches the gateway on the plain HTTP listener only, so the name is not one it can carry, and the process refuses it outright to any request bearing cf-connecting-ip.

    • The Host header is the only thing that names a site. KTHX_ZONE says which zone the process answers for; Host === $KTHX_ZONE is the apex, <name>.$KTHX_ZONE is that site, Host === $KTHX_CONTROL_HOST is the apex with the control API, and anything else is a 404. With the private host set, the public apex answers 403 PRIVATE to everything under /api/sites but the directory, and the landing page it serves hides the claim deck.

  • Where bytes live

    • An upload is normalised to a deterministic tar.gz, uploaded to the bucket as releases/<sha256>.tar.gz, then unpacked to /sites/<name>/<n>/ on a local-path volume and served from disk with Bun.file. The bucket is durable and content-addressed; the volume is a cache. A release whose directory is missing is refetched from its recorded location on the next request, so losing the volume costs latency and never data.

    • serving is a column on the site row β€” one integer says which release answers. A new upload serves on arrival unless held; rolling back points the column at an older number and sets the latch, and releasing the latch jumps to the newest.

    • The volume and the database both live on Fleet/oldschool, because local-path is a directory on one node's disk. The pod and the Postgres instance are pinned there by node selector for the same reason.

  • A database per site

    • Every claimed name gets a Postgres DATABASE and a LOGIN role of its own on the kthx-db cluster, created over SQL by the server at claim time from a template that already holds the documents table and its indexes. There is no DDL on the request path. The role's password is derived from KTHX_PG_KEY rather than stored, so a restore re-derives every one of them.

    • That is what /api/db writes into: collections of JSON documents, an etag over each document's canonical form for compare-and-swap, and a query grammar of equality and comparison operators over dotted paths. A site is bounded rather than trusted β€” a document is at most 1 MiB and a site's database at most 256 MiB, measured with pg_database_size.

    • The operator's own DELETE /api/sites hard-deletes every site at once β€” rows included, so those names come free again. It is gated on KTHX_ADMIN_KEY (the admin-key field of the kthx 1Password item, carried by the kthx-env ExternalSecret) and answers 404 where that is unset. kthx nuke is the same call; the landing page hides its control until a tab has been given the key.

    • Deleting a site drops the database and the role and removes its files; the name stays taken forever and answers 410. The nightly pg_dumpall into the bucket is the only undo path β€” see Runbooks/Managed Postgres for reaching either cluster.

  • The public shape

    • The private host speaks /api/sites…: claim a name, read a site, upload a release, point serving at one, drop the hold, delete. Claiming asks for no credential beyond reaching the host and returns a bearer once; everything after it is Authorization: Bearer, one bearer per site, and a lost bearer is a lost site. The public apex keeps the page, the directory, /sdk.js, /skill.md and /cli/kthx.tgz. A kthx command line or an agent publishes with KTHX_ORIGIN=https://kthx.${SECRET_DOMAIN}; GET /api on either apex answers {zone, url, docs}, and the CLI asks it for the zone, so kthx dev and kthx open still find the site β€” a committed kthx.json holds the name alone and never chooses where the bearer goes.

    • Every site host answers /api/db (documents), /api/ws (subscriptions and rooms), /api/me (a signed anonymous visitor id), /api/ai (an OpenAI-compatible passthrough on the operator's key, budgeted per site and per day), /api/files with the bytes at /files/* (a store anyone on the origin may add to, each path owned by the visitor who created it), /api/mcp (the site as an MCP server, owner-only) and /api/sdk.js. window.kthx fronts them; packages/kthx/sdk.js is the surface and packages/kthx/skill.md is the reference an agent reads.

    • Writes are open to anyone on the site's own origin, which is the point of the zone β€” a kthx link is sendable to anyone. What bounds it is not sign-in: a same-host Origin is required on every non-GET, three rate-limit buckets sit in front of writes, and the per-site ceilings above are the real limit.

    • /_/* and the apex /kthx/* answer 410. The name is the whole of the compatibility surface: a site claimed before the rewrite keeps serving its files under the same name and the same bearer.

  • Honest divergences

    • One replica. Presence, room fan-out, and the rate-limit buckets are in the process, so the design assumes exactly one of it β€” Recreate on the Deployment, not a rolling update. Postgres LISTEN/NOTIFY between replicas is the upgrade the code is shaped for; nothing needs it yet.

    • One node, one disk. Both volumes are local-path on Fleet/oldschool. Losing that host takes the zone down until it comes back, and the sites volume's declared size is not enforced by anything β€” local-path shares the node's filesystem, so what warns first is a node-disk alert rather than the claim.

    • No point-in-time recovery. One pg_dumpall a night, kept 30 days by a bucket lifecycle rule. A site's documents between two dumps are not recoverable.

    • Reach is the only identity. Anyone who can reach the private host β€” the lab, the offsite LAN, the tailnet β€” can claim a name; there are no owner accounts, no rotation, and no way to hand a site over. That is a deliberate floor, not an omission, and the caps in apps/kthx/server/limits.ts are what stands in for the rest. Every caller on the private host shares one address for those caps, because nothing in front of it sets cf-connecting-ip.

  • Cut-over

    • scripts/kthx-carry-over.sh moves site and release rows out of Spindrift's database into this one and copies their release objects into the kthx bucket; scripts/kthx-verify.sh runs the whole product against production on a throwaway name and deletes it afterwards. Both are one-shot operator scripts, run by hand, and both explain in their own headers why they are not declared Kubernetes objects.

Linked references 2

Architecture/Spindrift

Architecture/kthx is a neighbour, not a feature: its own process, its own namespace, its own database and bucket. What the two share is packages/archive/ β€” one reader for an uploaded tar or zip, one parser for a gs:// address β€” and the Apps Gateway that fronts both. Neither reads the other's rows.

Architecture

Architecture/kthx β€” quick sites with a database, a socket and a visitor identity, at <name>.kthx.dev