Onset Migration Plan — R940 Full-Stack Sovereignty (Tier 4 / SPRINT-6)¶
Status: T-3 CLOSED (Gate F, 2026-08-30). STOP — await Ryan T-4 go.
Parent audit: TIER_4_ONSET_MIGRATION_AUDIT.md
T-1 report: TIER_4_ONSET_T-1_REPORT.md
T-3 closeout: TIER_4_ONSET_T3_GATE_F_REPORT.md
T-4 plan: TIER_4_ONSET_T4_EXECUTION_PLAN.md
Pattern reference: REALOUTCOMES_MIGRATION_PLAN.md
Scope lock (Ryan decisions)¶
| Decision | Choice |
|---|---|
| VMIDs | 118 onset-app, 119 onset-db |
| User reality | Pre-user — operator testing only; no cutover comms |
| Cutover window | Sun 03:00–07:00 PDT (clinic DNS + pg_cron vault) |
| PHI keys | Migrate-as-is + mandatory Gate B decrypt smoke test |
| Auth | Clinic-first OIDC; GoTrue retained until iOS App Store release |
| iOS | TestFlight → App Store before hosted Supabase teardown |
| Cross-stack | Clinic on R940 while iOS prod stays on hosted Supabase until App Store |
| Staging hosted | Prod-first; tear down Onset-staging with prod at T-5 |
| Soak | Compressed (pre-user); gates unchanged for PHI, TestFlight, cross-stack |
| Privacy-service split | Out of scope for migration |
Constraints: Do not touch admin (paused), lastvet-edge, last1id-edge, realoutcomes-edge.
Target architecture¶
Cloudflare DNS (T-4)
│
onset-edge tunnel (VM 118)
│
┌────────────┴────────────┐
│ VM 118 onset-app │
│ Next.js clinic :3000 │
│ Clinic auth → auth.last1.id (T-3) │
└────────────┬────────────┘
│ LAN
┌────────────┴────────────┐
│ VM 119 onset-db │
│ Kong :8000 (LAN) │
│ Postgres 17 + pg_cron │
│ 24 Edge Functions │
│ GoTrue (until App Store) │
└────────────────────────────┘
iOS App Store prod (until T-5) ──► hosted Supabase emxrmjys… (rollback net)
TestFlight (T-4b) ──────────────► R940 Kong public hostname or tunnel route
onset-web (CF Pages) ───────────► update env at iOS/App Store cutover
Phase map¶
| Phase | Goal | Exit criteria | Ryan gate |
|---|---|---|---|
| T-0 | Audit + sub-audits | Done | Approved |
| T-1 | Planning + BWS layout | This plan + T-1 report | Now — T-2 go |
| T-2 | VMs + Supabase + Gate B + BWS | R940 DB row parity; PHI decrypt smoke PASS; 24 EF smoke | Per sub-gate |
| T-3 | Clinic app on VM 118 + clinic OIDC | Ryan logs into clinic via auth.last1.id; practitioner FKs intact; iOS-on-R940 auth proven (Gate E) | Done — Gate F |
| T-4 | onset-edge + clinic DNS + vault cron + pre-cutover fixes |
Clinic prod on R940; crons fire; storage parity; Sun window | DNS + cron verify |
| T-4b | TestFlight → R940 Supabase | Operator test on TestFlight build | TestFlight PASS |
| T-5 | App Store iOS + GoTrue retirement + teardown | iOS prod on R940; archive; GG close; hosted delete | Irreversible |
Calendar (compressed): ~7–10 working days after T-2 sign-off (vs RealOutcomes ~5–8).
T-2 — Database + Supabase stack (VM 119)¶
- Provision
onset-db(VMID 119, LAN 192.168.1.102/24). Planned.100is Dell iDRAC BMC (not available for VMs). - ZFS dataset
tank-fast/onset-db100 GB (20 MB DB today; headroom for Storage + logs). - Install official Supabase Docker Compose (pin via
.supabase-version; Postgres 17). - Configure:
- Realtime: disable (no client usage audited).
- Studio: LAN/Tailscale only.
- Storage: local filesystem; migrate buckets from hosted if any objects appear at Gate B.
- GoTrue: enabled through T-5 App Store gate (Ryan Q4).
- Sync 24 Edge Functions to
volumes/functions/: - 20 from
onset-ios/supabase/functions/(incl._shared/) - 4 from
clinic.theonset.app/supabase/functions/ - Hosted-only
auth-send-email: export from dashboard or recreate from GoTrue hook config at T-2. .env.functions/ compose secrets: Anthropic, Apple, Google, Last1 OAuth, assertion secret, SendGrid (notifications EF),PATIENT_PII_ENCRYPTION_KEY.- Gate B:
pg_dumphostedemxrmjysvxeboavhatry→ restore into R940. - Gate B PHI smoke (mandatory):
- Load same
PATIENT_PII_ENCRYPTION_KEYfrom Railway/BWS into R940 clinic env + EF secrets. - Decrypt all 5
practitioner_patientsrows viascripts/backfill-patient-pii.mjs --verifyor dedicated smoke script. - Assert
patient_email_hashunchanged (SHA-256 blind index). - Replay 3 pg_cron jobs from restored
cron.job; verifyvault.secretsforclinic_cron_secret,clinic_portal_base_url. - Edge Function smoke matrix (HTTP 200/expected errors) for all 24 names.
- Generate new JWT/anon/service keys → BWS
ONSET_APP_PROD_*(do not reuse hosted JWT post-cutover).
Scripts (T-2 deliver):
| Script | Purpose |
|---|---|
scripts/onset/provision-vm-supabase.sh |
VM 119 |
scripts/onset/provision-vm-app.sh |
VM 118 |
scripts/onset/setup-supabase-stack.sh |
Compose + functions sync |
scripts/onset/gate-b-rehearsal.sh |
Dump/restore |
scripts/onset/gate-b-phi-decrypt-smoke.sh |
PHI acceptance |
scripts/onset/sync-app-bws-from-railway.py |
Railway clinic env → BWS |
scripts/onset/push-db-stack-bws.py |
Stack-generated keys → BWS |
scripts/onset/smoke-edge-functions.sh |
24-function matrix |
T-2 — App VM (VM 118) parallel prep¶
- Provision
onset-app(VMID 118, LAN 192.168.1.101/24). - Docker build
clinic.theonset.appon VM. - Render
/opt/onset-clinic/.envfrom BWS (LANSUPABASE_URL=http://192.168.1.102:8000). - Do not cut prod DNS until T-4.
T-3 — Clinic auth unification (clinic-first) — COMPLETE¶
Closeout: TIER_4_ONSET_T3_GATE_F_REPORT.md (Ryan signed Gate E 2026-08-30)
See TIER_4_ONSET_T-1_REPORT.md §4.
Summary:
| Surface | T-3 outcome | GoTrue |
|---|---|---|
| Clinic portal | @last1id/sdk OIDC + clinic_last1_session |
Clinic login retired |
| iOS prod (App Store) | Unchanged hosted Supabase | Hosted GoTrue until T-5 |
| iOS-on-R940 | Proven Gate E E-3 (Apple Sign-In, TU6S85XABQ) |
VM 119 GoTrue — de-risks T-5 |
| Edge Functions (JWT) | Service role + user JWT from GoTrue until iOS cutover | VM 119 GoTrue |
FK remap (Gate D): curryrg@gmail.com → 2a805b21-5352-4316-bb4b-a5d4f02a8173; hello@unicornryda.com remains GoTrue/iOS test account.
Gate E temp state: R940 Staging.xcconfig pointer and GitHub APPLE_PRIVATE_KEY secret reverted/removed before sign-off. Staging xcconfig again targets hosted lzpyfohgfdohrtlamzek.
T-3 backlog (not blocking T-4 kickoff): notes-editor/template-actions anon client RLS risk; deferred api/clinic/register; dead lib/clinic-mfa.ts. /patients/new GoTrue bootstrap → T-4 pre-cutover (T4-0c).
LastVet: last1-connect-assertion keeps ONSET_IDENTITY_ASSERTION_SECRET synced with LastVet consumer before T-4.
T-4 — Edge + clinic DNS cutover¶
Execution plan: TIER_4_ONSET_T4_EXECUTION_PLAN.md
T4-0 — Pre-cutover fixes (MUST before DNS)¶
Ryan accepted Gate B gaps for T-3; must close before cutover:
| Fix | Current R940 | Target | Reference |
|---|---|---|---|
| pg_cron | 0 jobs | 3 jobs (clinic_weekly_summary, clinic_protocol_deliveries, clinic_fullscript_sync) |
T-3 sub-audits §B |
| storage.objects | 0 | 20 objects (~3.2 MB, 2 buckets) | T-3 sub-audits §A |
/patients/new bootstrap |
Client supabase.auth.getUser() |
Migrate to Last1 session / service role | Gate E deferred gap |
T4-1 — Tunnel + DNS¶
- Install
onset-edgecloudflared on VM 118. - Ingress:
clinic.theonset.app→http://127.0.0.1:3000. - Optional:
onset-api.last1.enterprisesor Kong route for TestFlight Supabase (Ryan picks at T-4b gate). - Ryan DNS CNAME →
{tunnel-uuid}.cfargotunnel.com(Sun 03:00–07:00 PDT). - Update vault secrets on VM 119:
clinic_portal_base_url=https://clinic.theonset.appclinic_cron_secret= matches R940CRON_SECRET- Stripe / Fullscript webhooks → R940 clinic URL.
- Do not update iOS App Store build or
onset-webprod Supabase env yet. - Railway + hosted Supabase remain live for iOS rollback.
Cross-stack divergence (accepted)¶
After T-4, clinic writes go to R940 Postgres; iOS App Store still reads/writes hosted Postgres. Acceptable because:
- No active external users.
- Only operator test (
hello@unicornryda.com) may use iOS on hosted until TestFlight. - TestFlight build switches to R940 before App Store promotion.
T-4b — TestFlight verification¶
Gate E already proved Sign in with Apple on R940 GoTrue (device pass, key TU6S85XABQ). T-4b validates the TestFlight channel on R940 config.
- Build iOS with R940
SUPABASE_URL+ new anon key (TestFlight channel). - Ryan operator smoke: sign-in (GoTrue on R940), check-in, protocol view.
- Clinic portal E2E on prod DNS (already on R940 from T-4).
- Gate: TestFlight PASS before App Store submission.
T-5 — App Store + teardown¶
- Ship App Store release with R940 Supabase URL/anon key.
- Update
onset-webCF Pages prod env to R940. - Retire VM 119 GoTrue (after iOS prod confirmed).
- Archive Railway + hosted Supabase (
backups/onset-teardown-YYYY-MM-DD/). - Delete Railway services + hosted projects (
emxrmjysvxeboavhatry,lzpyfohgfdohrtlamzek). - Close GG 29340466, 34103515–17; scrub invalid doc credentials 34103516–17.
- Remove legacy unprefixed BWS keys; publish runbook.
pg_cron migration (3 jobs)¶
| Job | Schedule (UTC) | Mechanism | T-4 action |
|---|---|---|---|
clinic_protocol_deliveries |
0 * * * * |
trigger_clinic_cron_route('/api/cron/protocol-deliveries') |
Vault URL → R940 clinic |
clinic_fullscript_sync |
15 * * * * |
…/fullscript-sync |
Same |
clinic_weekly_summary |
0 13 * * 1 |
Legacy hardcoded URL in older migration | Refactor to trigger_clinic_cron_route('/api/cron/weekly-summary') if not already unified |
Vault secrets (restored from dump + updated at cutover):
| Secret | Must match |
|---|---|
clinic_cron_secret |
Railway CRON_SECRET → R940 CRON_SECRET |
clinic_portal_base_url |
https://clinic.theonset.app |
T-2 verification: select cron.schedule_in_database(...) or manual select trigger_clinic_protocol_deliveries() against staging clinic hostname on R940 before T-4 prod DNS.
BWS key inventory¶
Recommendation: Use existing Bitwarden onset project (confirm UUID at T-2 kickoff). Add prefix ONSET_APP_PROD_* alongside any legacy keys; delete legacy at T-5.
Cross-project references:
| Secret | BWS project | Key |
|---|---|---|
| OAuth app secret (auth.last1.id) | Last1.id | LAST1_ID_R940_PROD_ONSET_APP_SECRET |
| Platform assertion (LastVet bridge) | Last1.platform (or HQ) | LAST1_PLATFORM_SHARED_ONSET_IDENTITY_ASSERTION_SECRET |
ONSET_APP_PROD_* target keys¶
| Key | Source / notes |
|---|---|
ONSET_DB_PROD_DATABASE_URL |
Generated VM 119 (LAN) |
ONSET_DB_PROD_SUPABASE_URL |
http://192.168.1.102:8000 internal |
ONSET_DB_PROD_SUPABASE_ANON_KEY |
Generated at stack init |
ONSET_DB_PROD_SUPABASE_SERVICE_ROLE_KEY |
Generated at stack init |
ONSET_DB_PROD_JWT_SECRET |
Generated at stack init |
ONSET_APP_PROD_PATIENT_PII_ENCRYPTION_KEY |
Railway sync — migrate unchanged (Q6) |
ONSET_APP_PROD_CRON_SECRET |
Railway sync |
ONSET_APP_PROD_STRIPE_* |
Railway sync; rotate webhook at T-4 if desired |
ONSET_APP_PROD_SENDGRID_API_KEY |
Railway / Supabase |
ONSET_APP_PROD_FULLSCRIPT_* |
Railway |
ONSET_APP_PROD_ANTHROPIC_API_KEY |
Supabase dashboard export |
ONSET_APP_PROD_NOTIFY_FUNCTION_SECRET |
Railway + EF |
ONSET_APP_PROD_PROTOCOL_IMPORT_SECRET |
Railway |
ONSET_APP_PROD_ONSET_IDENTITY_ASSERTION_SECRET |
Platform shared copy |
ONSET_APP_PROD_LAST1_OAUTH_CLIENT_ID |
onset |
ONSET_APP_PROD_LAST1_OAUTH_CLIENT_SECRET |
Last1.id project copy |
ONSET_APP_PROD_LAST1_ID_ISSUER |
https://auth.last1.id |
ONSET_APP_PROD_NEXT_PUBLIC_* |
Build-time clinic + CF Pages |
ONSET_APP_PROD_EDGE_CLOUDFLARED_TUNNEL_ID |
T-4 onset-edge |
ONSET_APP_PROD_PUBLIC_APP_URL |
Staging hostname then https://clinic.theonset.app |
Deferred Tier 6: ONSET_SUPABASE_* in admin BWS (last1-admin reconnect).
GitGuardian D-live rotations¶
| GG ID | Secret | T-2/T-5 action |
|---|---|---|
| 29340466 | Supabase JWT (prod xcconfig) | New R940 keys at cutover; RESOLVE after App Store |
| 34103515 | Supabase JWT (staging xcconfig) | Rotate at staging teardown T-5 |
| 34103516 | Postgres (docs) | IGNORE/close — invalid |
| 34103517 | Postgres (docs) | IGNORE/close — invalid |
Railway-only secrets (rotate opportunistically at T-4): Stripe webhook, SendGrid (no GG for clinic repo; still rotate if Ryan wants clean cutover).
Rollback¶
| Point | Action |
|---|---|
| Before T-4 DNS | Stop R940 containers; zero user impact |
| After T-4 clinic DNS | Repoint clinic.theonset.app to Railway; revert vault URL to Railway |
| After T-5 hosted delete | Irreversible — Gate B archive required |