flowchart LR
MAIN[push to main] --> CI["ci.yml<br/>lint · typecheck · tests<br/>content validate + build with a throwaway key<br/>web build · Playwright render-loop test<br/>API bundle"]
CI -- passes --> D["deploy.yml"]
D --> API["API + jobs<br/>remote Docker build<br/>release command: migrations<br/>check /statusz"]
API --> WEB["website + Worker<br/>source maps → Sentry<br/>wrangler deploy"]
WEB --> CON["content<br/>sign with the trusted key<br/>upload changed files · switch manifest"]
TAG["app-v* tag"] --> MOB["mobile.yml<br/>iOS + Android builds"]
MOB --> TF[TestFlight]
MOB --> FAD[Firebase App Distribution]
| Target | Trigger | Steps | Rollback |
|---|---|---|---|
| API and jobs | CI passes on main (or run by hand) | Docker build, migrations as release command, deploy, then /statusz must answer | Redeploy the previous image (migrations are backward compatible) |
| Website and Worker | After the API | Build, upload source maps (fails if the token is missing), wrangler deploy | Redeploy the previous version |
| Content | After the website | Build, sign, upload changed files, switch manifest | rollback switches the manifest back instantly |
| Android | app-v* tag or by hand | Signed build, then Firebase App Distribution to the founder or friends group | Previous build stays installable; min can be held back |
| iOS | app-v* tag or by hand | Archive with cloud-managed signing, upload to TestFlight | As above |
Deploys run one at a time and only when a repository switch enables them, so the pipeline never half-runs for want of secrets.
latest and min per platform and release notes in English and Hinglish, so update prompts change without an app release.timeline
title Release flow of one app build
Build tagged : CI builds iOS and Android
Testers : TestFlight and Firebase groups
Content publish : release info latest = N
Old builds : banner below latest
Breaking change : min raised, blocking screen below min
decision Deploys are boring on purpose. Every deploy is the same pipeline, every migration works with the previous build, and content can roll back in seconds. Shipping several times a day is safe because undoing is cheap.
Fly moves traffic to a new machine only after its health check passes, and a failed check stops the rollout with the old machines still serving. Today /readyz checks only that the database answers; after the deploy, CI calls /statusz, which also checks jobs and partitions.
${sch.nav()}