Everything you need to deploy, configure, and keep Oslr running. Oslr is a mobile-first app that helps doctors and medical students organise clinical bedside teaching on hospital wards — one React codebase shipped to web and to native iOS/Android.
All development lands via pull requests into dev; main is production and only moves when you merge dev → main. Full detail in Deployment → Branching & release workflow.
Branch
What it deploys
dev
Preview web (dev DB) · a release-candidate app to TestFlight for App Review — the app always points at the prod DB · the docs site
main
Production web + prod backend, and the reviewed app version is released to the stores at the same time
How it ships
Three pipelines, all triggered by pushing to GitHub. There are no manual release steps in the normal path.
Target
Tool
Trigger
Ships to
Web
Vercel
Push (any branch)
Website
Mobile
Codemagic
Push to dev
TestFlight + Google Play internal
Backend
GitHub Actions
Push touching supabase/**
Migrations, Edge Functions, email templates
Two environments
The backend runs as two separate Supabase projects, chosen by branch. The frontend picks its project from build-time VITE_SUPABASE_* variables.
Branch
Web (Vercel)
Supabase project
main
Production
prod
dev + feature branches
Preview
dev (jeibuakusgpcyybfnmos)
Secrets live in the provider dashboards (GitHub, Vercel, Codemagic, Supabase) — never in the repo. Ownership and rotation are in the Runbook.
Also in the repo
Architecture, conventions, and the data model live in CLAUDE.MD; the project overview is in the README. Point-in-time product notes (the July 2026 usability audit, action plan, and wave 1–2 fixes) and debugging reproduction scripts stay as markdown under docs/ — see the docs index.
01 · Operations
Deployment guide
How Oslr ships to web, mobile, and backend. For a command cheat-sheet see Quick reference; for first-time environment setup see Production setup.
Branching & release workflow
All development happens through pull requests into dev. Nothing is committed directly to main.dev is the integration branch that everyone builds on; main is production and is only updated by opening a release PR from dev → main.
Merging to…
Deploys
dev
Vercel Preview web (→ dev DB) · the dev Supabase project · a release-candidate app that Codemagic builds and uploads to TestFlight for App Review · the docs site (docs.oslr.co.uk)
main(via a dev → main PR)
Vercel Production web · the prod Supabase project (required reviewer) · and the reviewed app version is released to the stores at the same time
Intended: the native app is prod-backed on every track — Codemagic bakes the prod Supabase URL/key into every build, because the TestFlight build goes for App Review and needs a stable, populated backend. So the dev/prod split described below is a web concern; mobile should be prod for both the review build and the released version.
✅ Now prod-backed:codemagic.yaml injects the prod project values (mxthnwdweubpkjostkbd) into both mobile workflows, so the shipped/review build hits prod (#132 resolved). VITE_SUPABASE_ANON_KEY holds the project's publishable key (sb_publishable_…).
So a release looks like: push to dev → the app is built and submitted to TestFlight for review while you keep iterating on Preview web (dev DB). When the change is signed off, open a dev → main PR; merging it deploys production web + prod backend and releases the already-reviewed app version to the stores in the same step.
Overview
Three pipelines, all triggered by pushing to GitHub. There are no manual release steps in the normal path.
Target
Tool
Trigger
Ships
Web
Vercel
main → Production, other branches → Preview
oslr-web/ → website
Mobile
Codemagic
Push to dev
iOS → TestFlight, Android → Google Play internal
Backend
GitHub Actions
Push to main/dev touching supabase/**
Migrations, Edge Functions, auth email templates
Pull requests touching oslr-web/** also run a quality gate (.github/workflows/web-quality-gate.yml): npm ci, npm run build, npm run lint. This is what protects dev — a red gate blocks the merge.
Two environments
The backend runs as two separate Supabase projects, chosen by branch. This split governs the web app and backend deploys; the native app is always built against prod (see the note above).
Branch
Vercel
GitHub Environment
Supabase project
main
Production
Production
prod
dev + feature branches
Preview
Preview
dev (ref jeibuakusgpcyybfnmos)
There is no hardcoded fallback in oslr-web/src/lib/supabase.ts — a build with missing vars builds fine but white-screens at runtime with a "Missing Supabase configuration" error. Verify both scopes are set.
1 · Web (Vercel)
Two separate Vercel projects are built from this repo: the app (oslr-web/) and the marketing site (oslr-landing/). Each has its own vercel.json and its own Root Directory setting.
The app — oslr-web/
Config:vercel.json — SPA rewrites so client-side routes resolve.
Trigger: every push. main → Production, other branches → Preview URLs.
Build: Vercel runs npm ci + npm run build in oslr-web/, serves dist/.
The marketing site — oslr-landing/
Config:oslr-landing/vercel.json — rewrites to index.html for the client-side routes (/about, /help, /privacy, /terms), plus cache headers on /assets/*.
VITE_GA_MEASUREMENT_ID — the GA4 Measurement ID (e.g. G-XXXXXXXXXX). Set it in the Production scope only, so preview deploys never report into the analytics property. With it unset, the analytics code is stripped from the build entirely.
VITE_VIDEO_BASE_URL — the S3 base the /help page loads walkthrough videos from (defaults to the bucket convention in config.ts if unset).
Vite bakes both in at build time, so redeploy after changing either.
Analytics: GA4, loaded directly — there is no consent banner, and advertising signals are explicitly denied. Event schema, the one-time GA4 custom-dimension setup, and the two conversion funnels are documented in oslr-landing/README.md.
Help videos: the /help page is built from oslr-landing/src/lib/helpWorkflows.ts and embeds the demo videos from S3 (published by demo-videos/publish.sh) — see the Demo videos page.
Outbound links (app URL, store listings, contact addresses) all live in oslr-landing/src/lib/config.ts.
Environment variables (Vercel → Settings → Environment Variables — set each twice, once per scope):
Variable
Production scope
Preview + Development scope
VITE_SUPABASE_URL
https://<prod-ref>.supabase.co
https://jeibuakusgpcyybfnmos.supabase.co
VITE_SUPABASE_ANON_KEY
prod anon key
dev anon key
Vite bakes these in at build time — redeploy after changing them.
Manual deploy (rarely needed):
cd oslr-web
npm run build
vercel --prod
2 · Mobile (Codemagic)
Config:codemagic.yaml — two workflows, android_internal and ios_testflight.
Trigger: push to dev (both workflows) → iOS to TestFlight (submitted for App Review) and Android to the Play internal track.
Backend: the app is built against the prod Supabase project — codemagic.yaml supplies the prod VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY (mxthnwdweubpkjostkbd; the anon value is the project's publishable key) for both mobile workflows, so the review build hits prod.
Signing & secrets: managed in Codemagic (variable groups code-signing, ios-signing; Android keystore mercury_keystore; App Store Connect integration CodemagicOslr). No signing files are stored in the repo.
Release: the TestFlight/internal build is the release candidate. It goes public when you merge dev → main and release that version in App Store Connect / Play Console — the app store release and the production web/backend deploy happen together.
Both workflows run npm ci → npm run build → npx cap sync before building the native app.
Versioning is a single UTC timestamp, unified across platforms so a version is self-documenting (a user on 2026.0725.1430 was built 25 Jul 14:30 UTC):
Version name (iOS marketing + Android versionName, identical): YYYY.MMDD.HHMM.
iOS build (CFBundleVersion): YYYYMMDDHHMMSS.
Android versionCode (internal only): Google Play's latest build number + 1. Play's existing versionCode is already inflated near its ~2.1B cap — higher than the current Unix time — so a timestamp can't reliably beat it; we query the latest and increment. The user-visible versionName is still the unified timestamp above.
Builds a signed AAB (./gradlew bundleRelease) with the timestamp versionCode/versionName, publishes to the internal track.
Needs Play Developer API access for GOOGLE_PLAY_SERVICE_ACCOUNT.
iOS — ios_testflight
Bundle ID: com.mercury.mercuryapp; distribution type app_store.
Automatic signing via the App Store Connect integration (app-store-connect fetch-signing-files), CocoaPods install, build-ipa.
Sets ITSAppUsesNonExemptEncryption = false (no export-compliance prompt).
Release (two steps): after the IPA is built, Upload IPA to TestFlight uploads it with app-store-connect publish (codemagic-cli-tools, waiting for Apple to finish processing), then App Store version + metadata runs the Fastlane release lane to create the App Store version = this build's timestamp, attach the build, and push metadata from fastlane/metadata/** — leaving the version "ready to submit" (submit_for_review: false, so the final Submit stays a manual click; flip that flag to go fully hands-off). This is why the App Store version, not just the TestFlight build, now advances on every dev push.
Why the split: the binary upload uses the CLI (native ASC-key handling) rather than fastlane's upload_to_testflight, because Apple's altool rejects the SEC1-converted key that spaceship/deliver requires ("invalid AuthKey −39"). So the CLI does the upload and Fastlane (needs ruby: 3.3.6) does only the version + metadata via spaceship — no altool.
iOS code signing — what to do (step by step)
Signing here is automatic: Codemagic uses the App Store Connect API key (integration CodemagicOslr) to fetch/create the certificate and provisioning profile at build time — nothing is stored in the repo. The mental model that makes this painless:
A distribution certificate is account-wide — one certificate signs all your apps. It is not per-app.
A provisioning profile is per-bundle-ID, but the build auto-creates it (app-store-connect fetch-signing-files … --create).
The App ID com.mercury.mercuryapp already exists in the Apple account (from the original app), so a profile can be made for it.
One required one-time step: add CERTIFICATE_PRIVATE_KEY. Verified via the Codemagic API — the signing groups only held APPLE_TEAM_ID, and a test build failed instantly with "No matching profiles found for com.mercury.mercuryapp": Codemagic only had a stored provisioning profile for the old bundle ID. The pipeline now signs fully automatically via the App Store Connect API key (no stored profiles), which needs this one key. Do the three steps below once and it works for any bundle ID from then on.
Step 1 — Confirm the API key can manage signing. App Store Connect → Users and Access → Integrations → Keys → the key behind CodemagicOslr. Its role must be App Manager or Admin (a "Developer" key can't create certificates/profiles). Bump it if needed.
Step 2 — Generate a signing private key (one command on your Mac; copies itself to the clipboard):
Step 3 — Add it to Codemagic. Codemagic → oslr3 → Environment variables → add:
Name: CERTIFICATE_PRIVATE_KEY
Value: paste from the clipboard (the whole -----BEGIN…END----- block)
Group: ios-signing · tick Secure
That's it. The next build's Fetch signing files step creates (or reuses) the distribution certificate from that key and auto-creates the com.mercury.mercuryapp profile.
The certificate cap — what actually happened here (and will again if you rotate keys). Apple caps you at 2 Apple Distribution certificates. If the build fails at "Fetch signing files" with a 409 — "You already have a current Distribution certificate or a pending certificate request" — it means the key in CERTIFICATE_PRIVATE_KEY doesn't match either existing certificate, so --create tried to make a 3rd and Apple refused. A freshly-generated key won't match, and (unless you have the exact private key of a current cert) neither will an "old" one — so the reliable fix is to free a slot:
Recommended — revoke one, then re-run.Apple Developer → Certificates, revoke an old, unused Apple Distribution certificate (this does not unpublish or break apps already on the store), then re-run the build. With a slot free, --create mints a fresh certificate from your CERTIFICATE_PRIVATE_KEY, and every build after that reuses it automatically — no more matching or revoking.
See your certificates first (to pick a safe one to revoke): in the portal, or run app-store-connect certificates list in a one-off Codemagic step (it uses the same ASC API key, read-only) — it prints each cert's name, serial, and expiry.
Alternative — reuse a cert's key (no revoke), if you have it. Export the matching Apple Distribution cert from Keychain Access (login → Certificates → the entry with a key under it → Export .p12), extract the key — openssl pkcs12 -in cert.p12 -nodes -nocerts | openssl rsa -out dist_key.pem — and use it as CERTIFICATE_PRIVATE_KEY. Then fetch-signing-files finds the match and only creates the profile. In practice most people don't have the exact key, so revoking is usually simpler.
Push notifications
iOS push (FCM) needs the Firebase config to match the bundle ID. oslr-web/ios/App/GoogleService-Info.plist is the com.mercury.mercuryapp iOS app registered in the same Firebase project (oslr3-12501). If you ever change the bundle ID again, re-register that app in Firebase console → Project settings → your iOS apps and drop in the new GoogleService-Info.plist, or push will silently fail. Android is unaffected (already com.mercury.mercuryapp).
App Store listing as code
The App Store listing text is version-controlled in fastlane/metadata/en-US/ — name, subtitle, description, keywords, promotional_text, release_notes, privacy_url, and the support/marketing URLs. It is pushed with Fastlane deliver in two places, both with submit_for_review: false (they populate the listing but never submit):
Automatically on every dev push, by the release lane in ios_testflight (see above) — this keeps the prepared App Store version's text in sync with the repo as part of the normal build.
Manually, via the ios_appstore_metadata workflow (Codemagic UI → Start build → ios_appstore_metadata) — the metadata lane, for tweaking copy on the current editable version between builds without a rebuild.
Both lanes share one helper that handles Codemagic's @file: key reference and converts Apple's PKCS#8 .p8 to SEC1 (an OpenSSL 3 quirk); both need ruby: 3.3.6.
Because we ship as com.mercury.mercuryapp (the established app), deliver just updates the existing listing — the "Oslr" app-name clash and the brand-new-app "No data" first-version error that would hit a fresh app record both go away, so name/subtitle are now managed here.
Primary locale: metadata lives under en-US/ — it must match the app's primary locale in App Store Connect (English (U.S.), the locale the 3.0.0 version was created under). privacy_url points at https://www.oslr.co.uk/privacy; keep that page live or a submit will bounce.
An older appflow.yml / Disabled.codemagic.yaml.TMP remains from a previous Appflow setup — Codemagic is the live pipeline. Native builds and store uploads happen in Codemagic; opening ios/ in Xcode or android/ in Android Studio is for local debugging only.
3 · Backend (GitHub Actions → Supabase)
Config:.github/workflows/deploy-supabase.yml.
Trigger: push to main or dev that changes supabase/** (also manual via workflow_dispatch).
Environment:main → Production, otherwise → Preview. Environment secrets resolve the target project automatically, and protection rules (e.g. required reviewers on Production) gate the run.
What it does, against the selected project:
Installs the Supabase CLI and links the project.
supabase db push — applies migrations in supabase/migrations/.
Deploys every Edge Function with supabase functions deploy <name> --no-verify-jwt --use-api.
Sets the FCM_SERVICE_ACCOUNT_JSON function secret.
PATCHes the branded auth email templates (supabase/templates/*.html) via the Management API — only the mailer_* fields, leaving site_url/SMTP untouched.
Functions are deployed with --no-verify-jwt; those needing an authenticated caller (e.g. getLogs, deleteAccount) verify the JWT in their own code.
Required secrets. Repository-level (shared by both environments):
Secret
Source
SUPABASE_ACCESS_TOKEN
app.supabase.com/account/tokens
FCM_SERVICE_ACCOUNT_JSON
Firebase → Project settings → Service accounts
Per environment (Preview and Production, different values):
Secret
Value
SUPABASE_PROJECT_ID
that environment's project ref
SUPABASE_DB_PASSWORD
that project's database password
Manual deploy:
supabase login
supabase link --project-ref <project-ref>
supabase db push
supabase functions deploy <name> --use-api
Everyday workflow
Branch off dev, develop locally (npm run dev).
Open a PR into dev → the web quality gate runs; Vercel posts a Preview URL.
Merge to dev → deploys Preview web (dev DB) + the dev Supabase project, and Codemagic builds the release-candidate app and uploads it to TestFlight for App Review.
Release: once the app is reviewed and the change is signed off, open a PR dev → main. Merging deploys prod Supabase + Production web, and you release the reviewed app version in App Store Connect / Play Console at the same time.
A push to main can write to the live database. Keep a required-reviewer rule on the Production GitHub Environment so supabase/** changes pause for approval.
Verifying a deploy
Web
curl -I https://oslr3.vercel.app # expect 200
Check the deployment and logs in the Vercel dashboard.
Mobile
iOS: App Store Connect → TestFlight → check the new build processed.
Android: Play Console → Testing → Internal testing → check the versionCode.
Backend
GitHub → Actions → the "Deploy Supabase Changes" run is green.
Supabase dashboard → confirm the migration and function versions.
Rollback
Web — Vercel dashboard → Deployments → pick the last good one → Promote to Production (or vercel rollback).
Mobile — you can't truly roll back a shipped build; release a new build based on the previous good commit. On Android you can also halt/replace the internal-track rollout in the Play Console.
Backend — write a new "down" migration that reverses the change and supabase db push; never edit the original migration. For a function, git checkout <good-commit> -- supabase/functions/<name> and redeploy.
Troubleshooting
Symptom
Likely cause / fix
Web builds but white-screens
Missing VITE_SUPABASE_* in that Vercel scope — set both, redeploy
Supabase job runs on manual dispatch only
Push didn't touch supabase/** (path filter)
supabase db push fails
Migration error — test locally first with supabase db reset
Function deploy fails to bundle
Ensure the --use-api bundler is used (CI already does)
Android build can't read versionCode
Play Developer API access for the service account (Play Console → Setup → API access)
iOS signing fails
Check the App Store Connect integration + signing groups in Codemagic
Push notifications silent
Verify FCM_SERVICE_ACCOUNT_JSON set on the function's project
Commands and URLs at a glance. Full detail in the Deployment guide.
Branch → environment
Branch
Web (Vercel)
Supabase project
main
Production
prod
dev + features
Preview
dev (jeibuakusgpcyybfnmos)
Everything deploys on push, and all work lands via PRs into dev. A push to dev builds the app (prod-backed) to TestFlight for review; merging dev → main deploys production web + backend and releases the reviewed app. Web follows the branch (dev DB on Preview, prod DB on Production); the native app is prod-backed on every track (codemagic.yaml bakes in the prod project — see Deployment § Mobile).
The marketing site (oslr-landing/) is a second Vercel project in the same dashboard — see Deployment § 1 for its settings.
Deploy commands
Deploys are automatic on push. Manual fallbacks:
# Web
cd oslr-web && npm run build && vercel --prod
# Supabase (link the right project first)
supabase link --project-ref <project-ref>
supabase db push # migrations
supabase functions deploy <name> --use-api # a function
Every function is deployed with --no-verify-jwt; functions that need an authenticated caller verify the JWT in code.
Mobile (local, for debugging)
cd oslr-web
npm run build
npx cap sync ios && npx cap open ios # or android
Store builds/uploads run in Codemagic on push to dev, not locally.
curl -I https://oslr3.vercel.app # web up?
vercel logs # web logs
supabase functions logs <name> --project-ref <ref> # function logs
gh run list --workflow=deploy-supabase.yml # backend deploys
Rollback
# Web: promote a previous deployment in the Vercel dashboard, or
vercel rollback
# Supabase: reverse with a NEW migration (never edit an old one)
supabase migration new revert_<name>
supabase db push
Common issues
Issue
Fix
Web white-screens
Set both VITE_SUPABASE_* in that Vercel scope, redeploy
Backend deploy skipped
Push must change supabase/**
Migration fails
Test locally: supabase db reset
Android versionCode error
Grant Play Developer API access to the service account
iOS signing fails
Check Codemagic App Store Connect integration + signing groups
03 · Operations
Production / dev environment split
One-time setup of the two backend environments. For how deploys work day to day see Deployment; for a cheat-sheet see Quick reference.
Two Supabase projects, selected by git branch:
Branch
Vercel target
GitHub Environment
Supabase project
main
Production
Production
prod (new, clean)
dev (+ any feature branch)
Preview
Preview
dev (existing, ref jeibuakusgpcyybfnmos)
Migrations/functions deploy via .github/workflows/deploy-supabase.yml. Frontend env vars live in Vercel. There is no hardcoded Supabase fallback in the app (oslr-web/src/lib/supabase.ts) — a missing var fails loudly.
1 · GitHub — secrets & Environments
Repository → Settings → Secrets and variables → Actions, and Repository → Settings → Environments.
1a. Repository-level secrets (shared by both environments)
Secret
Value
Where to get it
SUPABASE_ACCESS_TOKEN
account access token
app.supabase.com/account/tokens (one token manages all your projects)
FCM_SERVICE_ACCOUNT_JSON
full service-account JSON (single line)
Firebase console → Project settings → Service accounts → Generate new private key
Share FCM only if dev and prod use the same Firebase project. For separate push projects, move FCM_SERVICE_ACCOUNT_JSON down into each Environment instead (env secret overrides repo secret of the same name).
1b. Environment Preview secrets (→ dev project)
Secret
Value
SUPABASE_PROJECT_ID
jeibuakusgpcyybfnmos (existing dev project ref)
SUPABASE_DB_PASSWORD
dev project DB password (Project Settings → Database)
1c. Environment Production secrets (→ prod project)
Secret
Value
SUPABASE_PROJECT_ID
new prod project ref (Project Settings → General → Reference ID)
SUPABASE_DB_PASSWORD
prod project DB password
Then on the Production environment:
Deployment branches and tags → Selected → add main. This restricts the Production environment (and its secrets) to the main branch.
Required reviewers → yourself. A push to main that touches supabase/** then pauses for your approval before it writes to the live DB.
Put the per-project values only in the environments. Don't leave SUPABASE_PROJECT_ID / SUPABASE_DB_PASSWORD at repo level too — a Preview job with an empty environment would silently fall through to the repo value. Keep only SUPABASE_ACCESS_TOKEN (+ FCM_SERVICE_ACCOUNT_JSON) at repo level.
GitHub CLI equivalent
# Repo-level (shared)
gh secret set SUPABASE_ACCESS_TOKEN
gh secret set FCM_SERVICE_ACCOUNT_JSON < fcm-service-account.json
# Per environment
gh secret set SUPABASE_PROJECT_ID --env Preview --body "jeibuakusgpcyybfnmos"
gh secret set SUPABASE_DB_PASSWORD --env Preview
gh secret set SUPABASE_PROJECT_ID --env Production --body "<PROD_REF>"
gh secret set SUPABASE_DB_PASSWORD --env Production
Environments must exist before --env works; create them once in the UI (Settings → Environments → New environment) or they'll 404.
2 · Vercel — frontend env vars
Project → Settings → Environment Variables. Add each variable twice — once scoped to Production (prod values), once scoped to Preview + Development (dev values). Same name, different value per scope.
Variable
Scope: Production
Scope: Preview + Development
VITE_SUPABASE_URL
https://<PROD_REF>.supabase.co
https://jeibuakusgpcyybfnmos.supabase.co
VITE_SUPABASE_ANON_KEY
prod anon key
dev anon key
Anon key: Project Settings → API → Project API keys → anonpublic.
Result: main deploys (Production) hit prod; dev and any feature branch (Preview) hit dev. vite dev locally uses .env.local (unchanged).
After changing Vercel env vars, redeploy — they're baked in at build time.
Mobile (Codemagic) env vars
Unlike Vercel, the mobile build is prod-backed: codemagic.yamlvars: set VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY to the prod project (mxthnwdweubpkjostkbd) for both workflows, so the TestFlight/internal build that goes for App Review talks to prod. The anon value is the project's publishable key (sb_publishable_…).
Run Phase 0 first (verify migrations rebuild the schema from scratch) before any of this.
# Link to the NEW prod project and push schema
supabase link --project-ref <PROD_REF>
supabase db push # applies all migrations to empty prod DB# Prod seed = reference data (job titles, specialties, locations) + two App# Review logins (a teacher + a student, on example.com, no org membership).# Locations are HOSPITALS (Broomfield only for launch). Do NOT use seed.sql or# seed_demo.sql (they insert demo sessions/orgs). Idempotent — safe to re-run.# NOTE: the main->Production deploy now applies this automatically (needs the# SUPABASE_DB_URL Production secret); this manual run is just for first setup.
psql "<PROD_DB_CONNECTION_STRING>" -f supabase/seed_prod.sql
# Edge functions + their secrets (functions don't carry secrets across projects)
supabase functions deploy --project-ref <PROD_REF>
supabase secrets set FCM_SERVICE_ACCOUNT_JSON="$(cat fcm-service-account.json)" --project-ref <PROD_REF>
Dashboard settings to replicate on prod (config.toml only covers local)
Auth → URL Configuration: Site URL = real prod URL; add prod redirect allow-list; set Apple sign-in redirect.
Auth → SMTP: configure custom SMTP + email templates (don't ship real users on Supabase's default rate-limited mailer).
Storage: recreate any buckets + policies the app expects.
Phase 0: supabase db diff --linked against dev is clean (no drift)
GitHub repo secrets set (access token, FCM)
GitHub Preview + Production environments created with their secrets
Production environment restricted to main + required-reviewer protection
Vercel vars set for both scopes, prod redeployed
dev branch created and pushed; verify it deploys to dev project
Prod schema pushed; seed_prod.sql applied (reference data + two App Review logins), functions + secrets deployed
seed_prod.sql auto-applies on every prod deploy using the existing SUPABASE_PROJECT_ID + SUPABASE_DB_PASSWORD secrets (via the session pooler); the optional SUPABASE_DB_URL secret only overrides a non-standard pooler host
Prod auth/SMTP/storage/backups configured
Mobile: Codemagic env vars set to prod Supabase; a dev build submitted to TestFlight for App Review, released to the stores when merged to main
End-to-end smoke test on prod (signup, RLS, push, account deletion)
5 · Hosting this documentation (docs.oslr.co.uk)
The doc site is served by its own Vercel project, separate from the app — so it has its own domain and deployments and never touches the app's. Set it up once:
Vercel → Add New → Project → import oslr/oslr3 again; name it e.g. oslr-docs.
Settings → Environments → set the Production environment's branch to dev (this is where the docs live). Note: the production-branch control moved to the Environments page — it is no longer under Settings → Git.
Settings → Domains → add docs.oslr.co.uk. At the oslr.co.uk DNS host add a CNAME: docs → cname.vercel-dns.com. Vercel issues the TLS cert automatically.
(Optional) Settings → Git → Ignored Build Step: git diff --quiet HEAD^ HEAD -- docs/site — rebuilds only when the docs changed.
Because this project tracks dev, docs.oslr.co.uk updates as soon as doc changes merge to dev. The site is a single self-contained index.html, so there is no build to configure.
04 · Operations
Release credentials & incident response
Who owns what, how often secrets rotate, and how to respond when a release breaks.
Ownership matrix
Domain
Primary owner
Backup / reviewer
Escalation
Codemagic iOS signing (cert/profile + App Store Connect integration)
Platform DevOps Lead
Security QA Lead
CEO
Codemagic Android signing (keystore + Google Play service account)
Platform DevOps Lead
Security QA Lead
CEO
GitHub Actions secrets (Supabase deploy + web quality gate)
Platform DevOps Lead
Security QA Lead
CEO
Vercel and Supabase deployment secrets
Platform DevOps Lead
Security QA Lead
CEO
Credential storage policy
Store production credentials in the team's password manager and platform secret stores only.
Never commit credential files or plaintext secrets to git.
Keep local credential artifacts in the git-ignored credentials/ directory when needed for manual setup.
Rotation cadence
Rotate mobile signing/API credentials every 90 days or immediately after team membership changes.
Rotate deployment tokens/secrets every 90 days.
Rotate immediately after any suspected leak, CI log exposure, or compromised workstation.
Pre-release checks
Confirm npm ci, npm run build, and npm run lint are green on the release commit.
Confirm Codemagic ios_testflight and android_internal workflows are green for the same commit.
Confirm the iOS bundle ID is com.mercury.mercuryapp and the Android package ID is com.mercury.mercuryapp.
Confirm Google Play upload track is internal and TestFlight upload completed.
Incident severity & response
SEV-1 — Production outage or broken mobile release pipeline
Trigger: release artifact cannot be produced or published for iOS/Android, or a production deploy failure blocks users.
SLA: acknowledge within 15 minutes.
Owner: Platform DevOps Lead.
Escalate to CEO immediately and request Security QA Lead for validation.
SEV-2 — Degraded CI signal or non-blocking release issue
Escalate to Security QA Lead if risk reaches release cutoff.
Incident procedure
Stabilize — freeze new release attempts; capture failing run links, logs, and the exact commit SHA.
Triage — identify whether the failure is credentials, configuration, or a source change; validate signing assets and secret availability in Codemagic/GitHub.
Mitigate — roll forward with a targeted fix when possible; if blocked, roll back to the last known good release commit and republish.
Communicate — post status updates every 30 minutes until resolved; include impact, current mitigation, and next checkpoint.
Recover & prevent — document root cause and preventive action; rotate any credentials involved; open follow-up tasks for hardening and test-coverage gaps.
Escalation contacts
Platform DevOps Lead — primary on-call for release engineering.
Security QA Lead — release gate and validation support.
CEO — final escalation for staffing, external communication, and approval.
Source of truth for this page: docs/site/index.html (runbook section).
05 · The app
Demo videos
Narrated walkthroughs of every key workflow. Each one drives a real browser through a real user story with a voiceover and captions — the fastest way to see how a part of the app actually works. Because every step asserts its way through the flow, a demo that renders is also a passing acceptance test of that story.
The videos are rendered by demo-videos/, committed as the source of truth, and auto-published to the oslr-demo-videos S3 bucket by a GitHub Actions workflow on push to main (npm run publish does it manually). The "Watch" links here — and the help site on oslr.co.uk/help — both embed the same S3 files, so there's one canonical copy. Setup, the render pipeline, and the publish step are in demo-videos/README.md.
The full catalogue — including which script covers each story and suggested groupings for longer films — is in WORKFLOWS.md. (There's also a 00-smoke clip that just exercises the render harness itself.)
Render them yourself
From demo-videos/, against a local dev server seeded with seed_demo.sql:
cd demo-videos
npm install && npx playwright install chromium
npm run list # list the demos
node render.mjs --check # headless, assert-only — fast acceptance test
npm run demo # re-seed + fixtures + render all
node render.mjs 01-create-teaching-session # render one → out/01-create-teaching-session.mp4
Prerequisites (dev server on :5173, demo accounts from seed_demo.sql, ffmpeg, optional OpenAI TTS) and how to add a new demo are in demo-videos/README.md. Keep each demo's selectors aligned with the matching cypress/e2e/*.cy.ts spec so the demo and the test stay in step.