How kitshn works
All of kitshn's logic and state run in one Cloudflare Worker with one Durable Object. Machines do only machine work: tunnels, Compose apps, local sites and commands.
The parts
kitshn Worker
Serves the map, checks Access tokens and GitHub OIDC tokens, and serves pages, files and the offline page.
Durable Object
Holds all state in SQLite. Keeps the sockets of the daemons and of open maps. An alarm queue writes changes to the Cloudflare API, at most 40 calls per run.
Cloudflare Access
Logs people in with an email code, GitHub and a passkey. It checks the circles at the edge, so a visit to an app never runs kitshn.
Daemon
One binary per machine. It runs cloudflared, deploys Compose apps in rootless Docker,
checks local sites, and keeps 30 days of samples on its own disk.
Recipe and CLI
kitshn.config.ts in the app repo. In GitHub Actions, kitshn check turns it
into JSON without running it, and kitshn deploy sends it.
GitHub App
Removes a preview when its pull request closes, writes deployment statuses, and starts the waiting run when you click Allow.
Circles become Access policies
Each circle becomes one reusable Access policy for its people, and one for its devices and networks. Each card with an address becomes one Access application, with the policies of the circles that hold it. A card in Everyone gets a Bypass policy.
kitshn is the source and Access is the copy. A daily check repairs differences, and kitshn never touches an Access object that it did not make. A lower level takes effect at once: kitshn ends the open sessions.
The tunnel of a machine checks the Access token too, so a wrong DNS record cannot skip the login.
A deploy, step by step
-
GitHub Actions reads
kitshn.config.tsas data and sends it with the run's OIDC token. kitshn accepts only repos of the GitHub accounts on its list. -
A repo that kitshn has never deployed waits for one click on Allow. After that, every push deploys. Block cuts a repo off at once.
-
A static site or Worker app: Actions uploads the files, and kitshn uploads the Worker through the Cloudflare API. Static requests are free.
-
A Compose app: kitshn picks the first online machine with the labels
deployand those of the recipe. The daemon gets the source through R2, checks the Compose file, builds at low CPU priority, and starts the app on a Unix socket. -
kitshn adds the Access application before the route, then the DNS record and the route. A removal runs the same steps in reverse, so an address is never open without its check.
Where state lives
| State | Lives in |
|---|---|
| Circles, members, shares, card places, history | Durable Object SQLite. Never in Git. |
| The recipe | The app repo. It is the only state in Git. |
| App values and secrets | Durable Object SQLite, secrets encrypted. KITSHN_* GitHub values win. |
| Pages and files | R2 |
| Access applications, tunnel routes, DNS records | Cloudflare, compiled from the map |
| Machine tokens and keys | A file on the machine that only the daemon user reads |
| Compose data and logs, sample history | The machine's disk |
It fits the free plan
The reference setup is 5 machines, about 15 apps, about 50 pages, 2 people and a few guests. Estimates for it:
| Limit | Free | kitshn uses |
|---|---|---|
| Worker requests | 100,000 a day | about 500 a day |
| Durable Object requests | 100,000 a day | about 4,200 a day |
| Access seats | 50 people | about 7. Guest seats free up after a month. |
| Service tokens | 50 | about 9: one per machine and device |
| Worker scripts | 100 | about 15. New previews stop at 90. |
App traffic goes through Access and tunnels, not through the kitshn Worker. A paid plan only raises limits; no feature needs it.
Read further
SPEC.md holds every numbered requirement. docs/architecture.md has the request paths and the full budget.