Order of a cross-repository release
When one feature touches several surfaces, release in this order:- Migration first. Additive only. The API version that is still running must work against the new schema.
- Backend second. New endpoints and fields are added. Old ones stay.
- Web app third. It deploys in minutes and every coach gets the new version on the next page load.
- Mobile last. An OTA update reaches a device after two launches. A store build reaches it when the trainee updates, which can take weeks.
- Clean up later. Remove an old endpoint or field only when no supported app version calls it.
mobile/scripts/ota-all-runtimes.sh.
Before any release
- The working tree of every repository you touched is clean and on the branch you expect.
- You pulled the latest
dev. - No secret, token or real customer data is in the diff.
- Translations were added for every locale the surface supports.
- The change was tested in Hebrew (RTL) and in one LTR language.
- Any rule that exists in more than one repository was changed in all of them. See Repositories and workspace.
Database
See Database migrations.- The model change is in both
packages/db/prisma/schema.prismaandpackages/database/prisma/schema.prisma. - A hand-written migration folder exists, and its timestamp sorts after the newest existing one.
- The SQL is safe to run twice (
IF NOT EXISTS) because the deploy workflow may push the column beforemigrate deployruns. - New columns are nullable or have defaults.
- A data-moving migration was replayed on a scratch database.
- A backup exists and the undo SQL is written down, for anything that rewrites or deletes data.
-
pnpm db:migrate:statusshows what you expect on the target database. -
pnpm db:migratewas run against the target database, and you checked the host inDATABASE_URLfirst. -
pnpm --filter @repo/database buildwas run and the result type-checks.
Backend
See Deploying the backend.-
pnpm cipasses locally: format check, lint, em-dash sweep, type-check, tests, build. - Every new environment variable is in the schema in
apps/core-api/src/config.tswith a safe default, and is set on the host before the deploy. - A new module is registered in
apps/core-api/src/modules/index.tsunder the correct lane. - A new route under
/v1/webhas a role guard where it needs one, and filters bystudioId. - A new worker is started in
server.tsand closed in both shutdown paths (shutdownand theSIGUSR2handler). - A new queue name is added to
PerformQueueinpackages/queue/src/index.ts. - Nothing in the change removes or renames a field or endpoint the mobile app uses.
-
TRAINEE_OTP_DEV_MODEis off on the host.
-
/healthzreturns200. -
/readyzreturns200with nodegradedentry. - The log shows
core-api listeningand noconfig validation failedoutput. - No burst of level 50 lines in the first minutes.
- A signed-in coach can load the trainee list (web lane).
- A trainee can open the app’s home screen (trainee lane).
- If the schema changed, a request that reads the new column succeeds.
Web app
See Deploying the web app.-
pnpm type-check,pnpm lintandpnpm testpass. -
pnpm check:themepasses. Check the screen in dark mode. -
pnpm check:core-apiwas run and you understand its output. -
pnpm buildsucceeds locally for any change tonext.config.ts,proxy.ts, the Dockerfile, or a server component’s imports. - The backend change it depends on is already deployed.
- If the trainee preview should change, the bundle was re-exported from the mobile repository and is part of the commit.
- If a
NEXT_PUBLIC_*value changed, the build arguments or build env were updated. A runtime env change is not enough.
- The landing page loads at
/when signed out. -
/api/healthon the web origin returnsOK. - Login works with a phone code or an email code.
- The trainee list, one trainee card, and one builder load.
- A browser tab that was open before the deploy still works after a refresh.
Mobile: OTA update
See OTA updates.- The diff contains no native change:
package.jsondependencies with native code,app.json,plugins/,modules/,patches/. - Every import of a native module added since the oldest runtime in the list is guarded with
requireOptionalNativeModule. -
pnpm typecheckandpnpm lintpass. - The relevant
node --test scripts/test-*.cjsfiles pass. - The production API already serves everything the bundle calls.
-
app.jsonis committed and shows the current version. - Risky change: published to
previewfirst and tested on a preview build, opened twice. - Published to the current version with
pnpm ota:production. - Published to older runtimes with
scripts/ota-all-runtimes.sh. -
eas update:list --branch productionshows each group under the right runtime. -
app.jsonis unchanged after the script finished. - Verified on an iOS device and an Android device, each relaunched twice.
Mobile: store build
See Mobile builds.-
versionis the same inapp.jsonandpackage.json. -
eas.jsonand.env.productionhold the same public values. - A development or preview build with the new native code was tested on a real device on both platforms.
- Push notifications, sign-in, a workout, a meal log and a form submission were tested on the build.
- HealthKit on iOS and Health Connect on Android were tested if anything near them changed.
- RTL and LTR were both checked on a real build. Expo Go does not apply native RTL.
- The build was submitted, and the store review notes mention the review login if the reviewer needs it.
-
TRAINEE_APP_IOS_VERSIONandTRAINEE_APP_ANDROID_VERSIONon the backend were raised to the new version, each only after its own store serves the build. - The previous version was added to the runtime list in
scripts/ota-all-runtimes.sh. - Runtimes with no remaining installs were removed from that list.
- The first OTA update for the new version was published and verified, so the update path is known to work before it is needed.
If something goes wrong
Rolling back code never rolls back the database. That is why step one of every release is an additive migration.