Skip to content

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)

  1. Provision onset-db (VMID 119, LAN 192.168.1.102/24). Planned .100 is Dell iDRAC BMC (not available for VMs).
  2. ZFS dataset tank-fast/onset-db 100 GB (20 MB DB today; headroom for Storage + logs).
  3. Install official Supabase Docker Compose (pin via .supabase-version; Postgres 17).
  4. Configure:
  5. Realtime: disable (no client usage audited).
  6. Studio: LAN/Tailscale only.
  7. Storage: local filesystem; migrate buckets from hosted if any objects appear at Gate B.
  8. GoTrue: enabled through T-5 App Store gate (Ryan Q4).
  9. Sync 24 Edge Functions to volumes/functions/:
  10. 20 from onset-ios/supabase/functions/ (incl. _shared/)
  11. 4 from clinic.theonset.app/supabase/functions/
  12. Hosted-only auth-send-email: export from dashboard or recreate from GoTrue hook config at T-2.
  13. .env.functions / compose secrets: Anthropic, Apple, Google, Last1 OAuth, assertion secret, SendGrid (notifications EF), PATIENT_PII_ENCRYPTION_KEY.
  14. Gate B: pg_dump hosted emxrmjysvxeboavhatry → restore into R940.
  15. Gate B PHI smoke (mandatory):
  16. Load same PATIENT_PII_ENCRYPTION_KEY from Railway/BWS into R940 clinic env + EF secrets.
  17. Decrypt all 5 practitioner_patients rows via scripts/backfill-patient-pii.mjs --verify or dedicated smoke script.
  18. Assert patient_email_hash unchanged (SHA-256 blind index).
  19. Replay 3 pg_cron jobs from restored cron.job; verify vault.secrets for clinic_cron_secret, clinic_portal_base_url.
  20. Edge Function smoke matrix (HTTP 200/expected errors) for all 24 names.
  21. 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

  1. Provision onset-app (VMID 118, LAN 192.168.1.101/24).
  2. Docker build clinic.theonset.app on VM.
  3. Render /opt/onset-clinic/.env from BWS (LAN SUPABASE_URL=http://192.168.1.102:8000).
  4. 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

  1. Install onset-edge cloudflared on VM 118.
  2. Ingress: clinic.theonset.app → http://127.0.0.1:3000.
  3. Optional: onset-api.last1.enterprises or Kong route for TestFlight Supabase (Ryan picks at T-4b gate).
  4. Ryan DNS CNAME → {tunnel-uuid}.cfargotunnel.com (Sun 03:00–07:00 PDT).
  5. Update vault secrets on VM 119:
  6. clinic_portal_base_url = https://clinic.theonset.app
  7. clinic_cron_secret = matches R940 CRON_SECRET
  8. Stripe / Fullscript webhooks → R940 clinic URL.
  9. Do not update iOS App Store build or onset-web prod Supabase env yet.
  10. 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.

  1. Build iOS with R940 SUPABASE_URL + new anon key (TestFlight channel).
  2. Ryan operator smoke: sign-in (GoTrue on R940), check-in, protocol view.
  3. Clinic portal E2E on prod DNS (already on R940 from T-4).
  4. Gate: TestFlight PASS before App Store submission.

T-5 — App Store + teardown

  1. Ship App Store release with R940 Supabase URL/anon key.
  2. Update onset-web CF Pages prod env to R940.
  3. Retire VM 119 GoTrue (after iOS prod confirmed).
  4. Archive Railway + hosted Supabase (backups/onset-teardown-YYYY-MM-DD/).
  5. Delete Railway services + hosted projects (emxrmjysvxeboavhatry, lzpyfohgfdohrtlamzek).
  6. Close GG 29340466, 34103515–17; scrub invalid doc credentials 34103516–17.
  7. 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