π¦kthx
kthx is quick sites: pick a name, drop a zip or an
index.html, and it answers athttps://<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 onkthx.${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.tsdispatches onHostβ the apex, a site host, or nothing β and everything else hangs off that:serve.tsanswers static bytes,sites.tsis claim/release/serve/hold/delete,documents.tsandpg.tsare the per-site database,realtime.tsis the socket,releases.tsowns the unpack and the volume,depot.tsreads and writes the bucket,me.tssigns the visitor cookie,limits.tsholds the buckets and caps. Its own schema is numbered SQL inapps/kthx/server/migrations/, applied at boot.apps/kthx/cli/is thekthxcommand:deployclaims a name when a directory has none and uploads,devserves a directory locally and proxies/api/*and/files/*to the site host so the page runs against its real database,rollbackandreleasedrive the hold latch. The name iskthx.jsonin 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 theclistage ofapps/kthx/Dockerfile. There is no npm package, sokthx upgradeis what replaces an installed copy: it re-runsbun add -gon that tarball. The build idapps/kthx/pack.tswrites intodist/version.jsontravels both inside the tarball and beside it in the image, which is what lets an installed copy compare itself againstx-kthx-buildon one cappedHEADa day and sayupdate available.packages/kthx/is what a browser gets:sdk.jsiswindow.kthx,landing.htmlis the apex page,skill.mdis the agent reference the apex serves, andfavicon.tsis the dot a site with no icon of its own answers with.packages/archive/holds the tar, zip and GCS code bothapps/kthxandapps/spindriftimport β one reader for an uploaded archive, one address parser for ags://location.packages/charts/kthx/andclusters/offsite/apps/kthx/are the workload: a Deployment, its Service and ServiceAccount, the sites volume, a CloudNativePGCluster, a nightly dump, two network policies, and the HTTPRoute.clusters/offsite/monitoring/kthx-rules.yamlis what pages about it. The zone and the bucket areterraform/network/cloudflare/kthx.dev.tfandterraform/gcp/projects/bluenose/.
How a name resolves
Cloudflare holds the zone and terminates TLS. The apex and a wildcard both CNAME to the
spindrift-controltunnel, which reachescilium-gateway-spindrift-appson 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 areno-cachewith 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.yamlcarrieskthx.devand*.kthx.devto the Service. The route is in thekthxnamespace and the gateway is not, so that namespace wearsapp.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.hostinclusters/offsite/apps/kthx/helm-release.yaml, pinned bysectionNameto 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 bearingcf-connecting-ip.The
Hostheader is the only thing that names a site.KTHX_ZONEsays which zone the process answers for;Host === $KTHX_ZONEis the apex,<name>.$KTHX_ZONEis that site,Host === $KTHX_CONTROL_HOSTis the apex with the control API, and anything else is a 404. With the private host set, the public apex answers403 PRIVATEto everything under/api/sitesbut 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 alocal-pathvolume and served from disk withBun.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.servingis a column on the site row β one integer says which release answers. A new upload serves on arrival unlessheld; 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-pathis 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
DATABASEand aLOGINrole of its own on thekthx-dbcluster, created over SQL by the server at claim time from a template that already holds thedocumentstable and its indexes. There is no DDL on the request path. The role's password is derived fromKTHX_PG_KEYrather than stored, so a restore re-derives every one of them.That is what
/api/dbwrites 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 withpg_database_size.The operator's own
DELETE /api/siteshard-deletes every site at once β rows included, so those names come free again. It is gated onKTHX_ADMIN_KEY(theadmin-keyfield of thekthx1Password item, carried by thekthx-envExternalSecret) and answers 404 where that is unset.kthx nukeis 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_dumpallinto 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, pointservingat one, drop the hold, delete. Claiming asks for no credential beyond reaching the host and returns a bearer once; everything after it isAuthorization: Bearer, one bearer per site, and a lost bearer is a lost site. The public apex keeps the page, the directory,/sdk.js,/skill.mdand/cli/kthx.tgz. Akthxcommand line or an agent publishes withKTHX_ORIGIN=https://kthx.${SECRET_DOMAIN};GET /apion either apex answers{zone, url, docs}, and the CLI asks it for the zone, sokthx devandkthx openstill find the site β a committedkthx.jsonholds 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/fileswith 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.kthxfronts them;packages/kthx/sdk.jsis the surface andpackages/kthx/skill.mdis 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
Originis 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 β
Recreateon the Deployment, not a rolling update. PostgresLISTEN/NOTIFYbetween replicas is the upgrade the code is shaped for; nothing needs it yet.One node, one disk. Both volumes are
local-pathon 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-pathshares the node's filesystem, so what warns first is a node-disk alert rather than the claim.No point-in-time recovery. One
pg_dumpalla 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.tsare what stands in for the rest. Every caller on the private host shares one address for those caps, because nothing in front of it setscf-connecting-ip.
Cut-over
scripts/kthx-carry-over.shmoves site and release rows out of Spindrift's database into this one and copies their release objects into the kthx bucket;scripts/kthx-verify.shruns 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/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/kthx β quick sites with a database, a socket and a visitor identity, at <name>.kthx.dev