1# Deploying dev.site23The control plane runs on [Fly](https://fly.io): one machine, one volume, SQLite on it.4`fly.toml` and `Dockerfile` in the repository root are the whole deployment.56One machine is deliberate. A Fly volume attaches to a single machine in a single region,7and nothing here wants a second writer. Since the heartbeat went away the database is8read-mostly — a few hundred bytes per account, and a handful of writes per user per9lifetime — so this shape has a lot of room in it.1011## The durable identity boundaries1213**OIDC issuer and subjects.** Accounts are keyed by the verified `(issuer, subject)` pair.14Shoo is the default adapter and derives its default `client_id` and pairwise subjects from15`DEVSITE_PUBLIC_ORIGIN`, so changing that origin after anyone has signed in still orphans16those identities. Another public OIDC client can be selected with the `DEVSITE_OIDC_*`17settings below without changing the account or session code.1819**The capability signing key.** Every daemon pinned its public half at `devsite login` and20refuses capabilities signed by anything else. Lose it and every daemon in the world stops21working, with no way to recover but re-running `devsite login` everywhere.2223So it is a Fly secret rather than a file on the volume: encrypted at rest, injected into24the machine's environment, and not tied to the survival of one volume on one host. The25server prefers `DEVSITE_SIGNING_KEY` when it is set and falls back to `DEVSITE_STATE_DIR`26when it is not, which is what keeps local development unchanged.2728## One-time setup2930```bash31fly apps create devsite3233# --yes answers the "every volume is pinned to one physical host, make two"34# warning, which cannot prompt without a terminal. One volume is the intended35# shape here: a second would be a second SQLite file on a second machine, both36# taking writes and neither aware of the other. Redundancy needs LiteFS, not37# another volume.38fly volumes create devsite_data --region sjc --size 1 --yes3940# 32 random bytes as 64 hex characters. Generate it here, keep a copy somewhere41# you would still have after losing this laptop, and never commit it.42fly secrets set DEVSITE_SIGNING_KEY="$(openssl rand -hex 32)"43```4445To carry an existing key over instead of generating one:4647```bash48fly secrets set DEVSITE_SIGNING_KEY="$(xxd -p -c 64 data/state/capability_signing.key)"49```5051Check it against what daemons pinned before trusting it — `fly logs` prints the public half52at boot, and `devsite status` prints what the daemon expects.5354### The hostname5556The first deploy provisions a shared v4 and a dedicated v6 by itself, so there is usually57nothing to allocate:5859```bash60fly ips list # the addresses to point DNS at61```6263At Cloudflare, on the `dev.site` zone, add `A` and `AAAA` records for the apex pointing at64those addresses, **DNS only — grey cloud, not proxied**. Fly needs to answer the ACME65challenge on the hostname itself; proxying breaks issuance and puts a second TLS terminator66in front of a site whose whole argument is about who can see what.6768```bash69fly certs add dev.site70fly certs show dev.site # until it reports Ready71```7273## Deploying7475```bash76fly deploy77```7879Fly builds the image remotely. You do not need a local Docker daemon. The build checks and80compiles the web source. It then builds the server and copies both outputs into the image.8182A deploy replaces the machine, which means a few seconds where the site is down. That is83the cost of one volume, and it is the right trade at this size.8485## Backups8687Fly snapshots volumes daily, five days' retention. That covers the machine dying; it does88not cover "I want the database on my laptop", which is worth having:8990```bash91fly ssh console -C "sqlite3 /data/devsite.db '.backup /data/backup.db'"92fly sftp get /data/backup.db ./devsite-backup.db93```9495The signing key is not on the volume and is not in these backups. Keep it separately — it96is the part that cannot be regenerated.9798## Configuration99100Set in `fly.toml`, except the secret.101See [Authentication providers](../docs/auth-providers.md) for replacing Shoo or implementing a102non-OIDC adapter.103104| Variable | Where | Notes |105| --- | --- | --- |106| `DEVSITE_PUBLIC_ORIGIN` | `fly.toml` | `https://dev.site`. See above. |107| `DEVSITE_OIDC_ISSUER` | optional | Token issuer; defaults to `https://shoo.dev`. |108| `DEVSITE_OIDC_AUTHORIZATION_ENDPOINT` | optional | Defaults to `<issuer>/authorize`. |109| `DEVSITE_OIDC_TOKEN_ENDPOINT` | optional | Defaults to `<issuer>/token`. |110| `DEVSITE_OIDC_JWKS_URI` | optional | Defaults to `<issuer>/.well-known/jwks.json`. |111| `DEVSITE_OIDC_CLIENT_ID` | optional | Public client id; defaults to `origin:<public origin>`. |112| `DEVSITE_OIDC_SCOPES` | optional | Space-separated; defaults to `openid` and must include it. |113| `DEVSITE_OIDC_ALGORITHMS` | optional | Allowed asymmetric JWT algorithms; defaults to Shoo's `ES256`. |114| `DEVSITE_SIGNING_KEY` | `fly secrets` | Hex. Never in the repository. |115| `IROH_SERVICES_API_SECRET` | optional secret | Iroh Services API key for scoped relay tokens. |116| `DEVSITE_RELAY_URLS` | optional | Comma-separated custom relay URLs. The n0 preset is the default. |117| `DEVSITE_BIND` | `Dockerfile` | `0.0.0.0:8080`, matching `internal_port`. |118| `DEVSITE_DB` | `Dockerfile` | `/data/devsite.db`, on the volume. |119| `DEVSITE_STATE_DIR` | `Dockerfile` | Unused while the secret is set. |120| `DEVSITE_WEB_ROOT` | `Dockerfile` | `/app/web`, baked into the image. |121122## What Fly sees123124Fly terminates TLS for the control plane, so it sees profile metadata and API calls — the125same position Cloudflare was in before. It never sees private service traffic: that goes126client-to-daemon over an Iroh relay, end-to-end encrypted, and the control plane is not127even told where the daemon is.128129## Caching130131Static files and the HTML shell revalidate; `/api/` responses are `no-store`.132133Cloudflare still holds the `dev.site` zone, but only as DNS — the `A` and `AAAA` records134point straight at Fly, unproxied.