Accepted
ADR 0030 — Pre-install public route redirect
Until setup completes, every public GET redirects to the first-run wizard
The Deploy to Cloudflare button lands a deployer on a workers.dev URL. The promise is that the Worker detects the empty database and routes the editor to the first-run setup wizard. The /admin path satisfies that on its own — it checks setup status and redirects — but the deploy URL is the site root, and an editor who opens it directly should not have to know that /admin exists. Cusp is an editor-first product whose pitch is going from clicking Deploy to a working CMS without touching a terminal, so every public entry point has to lead to the wizard.
The decision
When the install flag is not set, the public render handler short-circuits with a 302 to /setup. The redirect fires before the cache lookup, before the R2 lookup, and before live SSR.
The gate is scoped precisely so it never traps the routes that must work pre-install:
- The gate is the public render catch-all, which handles GET requests only. Any public GET that reaches it — including an unknown path that would otherwise fall through to static assets — is redirected to /setup.
- The admin API and headless API are mounted earlier on the router and never reach the public gate; their existing 401 and token flows keep working pre-install.
- The admin SPA routes (/admin/*), /login, and the /setup route itself are mounted earlier still, so the wizard and the SPA's own setup-status check keep their existing behaviour.
- Theme assets (/_themes/*) and media (/media/*) are also mounted before the catch-all, so their dedicated R2-backed handlers respond rather than redirecting. Those surfaces serve nothing meaningful pre-install anyway, so the behaviour is harmless either way.
The cached install flag
Setup status is a single D1 SELECT against the installed-setting row. Running that query on every public request forever would be wasteful, so the check is cached. The cache is a module-scoped boolean in the isolate: once a true read returns, it latches at true for the isolate's lifetime — installation never flips back. The first warm request after install pays for one SELECT; everything after is a single boolean read.
The setup route marks the cache immediately after a successful install so the same isolate skips the redundant SELECT on its next request. The cache never stores false: a false latch would keep redirecting after another isolate completed the install. Pre-install the flag is re-read every request — the wizard is a one-shot flow exercised by exactly one editor, and the cost is irrelevant against the wizard's own latency. The first read after any isolate installs returns true and latches.
Consequences
- The editor who lands on the raw deploy URL is taken straight to the wizard, with no need to know to type /admin.
- Search engines are unaffected — pre-install there is no content, no sitemap, and no inbound links; post-install the latched flag short-circuits the redirect logic entirely.
- Post-install the added cost is one boolean compare per public GET. Acceptable.
- Tests that exercise the public render path must install the site first, or reset the module cache between install and uninstall transitions.