Tier 4 T-1 Report — Onset Planning + BWS + VMIDs¶
Date: 2026-08-28
Status: T-1 COMPLETE. STOP here. Await Ryan authorization for T-2 (VM provision + Gate B + PHI smoke).
Parent audit: TIER_4_ONSET_MIGRATION_AUDIT.md
Execution plan: ONSET_MIGRATION_PLAN.md
Executive summary¶
T-1 confirms Option A (self-hosted Supabase full stack) on R940 is feasible for Onset, following the RealOutcomes VM 116/117 pattern at smaller scale (~20 MB DB, 24 Edge Functions, 3 pg_cron jobs).
User reality (final): Pre-user. Promo codes expired; no active external users. Only operator workflow (hello@unicornryda.com test). No cutover comms.
Auth (Ryan Q4): Clinic-first OIDC to auth.last1.id; GoTrue on VM 119 stays alive until iOS App Store ships. Expect a cross-stack window: clinic on R940, iOS App Store on hosted Supabase until TestFlight/App Store gates.
PHI (Ryan Q6): Migrate-as-is PATIENT_PII_ENCRYPTION_KEY with mandatory Gate B decrypt smoke test. No privacy-service validation during migration.
BWS: Use existing onset Bitwarden project (Ryan confirm UUID at T-2 kickoff) with new ONSET_APP_PROD_* keys; cross-project secrets on Last1.id + Last1.platform.
VMIDs (Ryan Q5): 118 onset-app @ 192.168.1.101, 119 onset-db @ 192.168.1.100.
Cutover window (Ryan Q2): Sun 03:00–07:00 PDT for clinic DNS + vault cron updates.
Pacing: Compressed soak (pre-user); gate discipline retained for PHI smoke, TestFlight, App Store, cross-stack management.
1. Final user inventory (Q1 closed)¶
| Email / ID | Classification | Cutover comms |
|---|---|---|
curryrg@gmail.com |
PRACTITIONER (Ryan) | None |
hello@unicornryda.com |
TEST (Ryan) — unicorn PC workflow | None (operator) |
Null Apple 474ef7c8-… |
PROMO EXPIRED — never engaged | None |
jrdelacruz.a@gmail.com |
PROMO EXPIRED — dormant one-time trial | None |
Apple relay d97ede01-… |
PROMO EXPIRED — never engaged | None |
ASC / RevenueCat: Promo codes expired; no active redemptions. Confirms pre-user posture.
TestFlight cohort: Operator only (Ryan) until external users exist post-launch.
2. VM planning (118 / 119)¶
Following RealOutcomes split: memory-heavy Supabase on 119, Next.js clinic + tunnel on 118.
| VM | VMID | Hostname | LAN IP | Spec | Role |
|---|---|---|---|---|---|
| Supabase | 119 | onset-db |
192.168.1.100/24 | 4 vCPU · 16 GB RAM · 50 GB boot + 100 GB ZFS (tank-fast/onset-db) |
Supabase Docker Compose, Postgres 17, Kong :8000, pg_cron, pg_net, 24 Edge Functions, GoTrue (until App Store), Storage |
| App | 118 | onset-app |
192.168.1.101/24 | 2 vCPU · 4 GB RAM · 50 GB boot | Next.js clinic Docker :3000, onset-edge cloudflared, node_exporter |
IP rationale: Next sequential after RealOutcomes .98/.99 and admin .97. Ryan confirm no collision on pve-r940 before T-2 provision.
Tailscale: Join both VMs; BWS DATABASE_URL uses LAN IP (portfolio convention).
Sizing notes:
| Factor | RealOutcomes 117 | Onset 119 |
|---|---|---|
| DB size | ~1.3 GB / 2M IRS rows | ~20 MB |
| Edge Functions | 8 | 24 (3× count; Anthropic-heavy) |
| ZFS data | 150 GB | 100 GB (sufficient) |
| RAM | 16 GB | 16 GB (keep; EF runtime headroom) |
Ingress: Per R940_PRODUCT_ISOLATION_PRINCIPLES_v1.md — dedicated onset-edge on VM 118. Do not add Onset routes to lastvet-edge.
Optional public Supabase hostname: Required for TestFlight iOS hitting R940 before clinic cutover if not using LAN-only builds. Recommend onset-db.last1.enterprises CNAME via onset-edge → Kong :8000 (T-4b decision). Clinic server uses LAN http://192.168.1.100:8000.
3. Self-hosted Supabase feasibility (Option A)¶
Verdict: GO, with T-2 proof tasks¶
| Capability | Feasibility | Notes |
|---|---|---|
| Edge Runtime (24 functions) | Yes | 20 from onset-ios + 4 from clinic.theonset.app; sync to volumes/functions/ |
| Storage | Yes | Audit small object count; local filesystem backend |
| GoTrue | Yes (phased retirement) | Lift auth.users (8 rows) in Gate B; retain through App Store for iOS/TestFlight |
| pg_cron (3 jobs) | Yes | Restored from dump; vault HTTP to clinic portal |
| Postgres 17 | Yes | Hosted 17.6.1; canonical migrations in onset-ios/supabase/migrations/ |
| Realtime | N/A | Disable in compose |
| RLS | Medium review | Mixed patient + clinic policies; clinic uses service role heavily; T-2 verify critical paths |
T-2 must prove (gaps)¶
| Gap | Mitigation |
|---|---|
| 24 EF vs 8 (RealOutcomes) | Batch smoke script; prioritize Anthropic + practitioner-notifications + Last1 bridge functions |
auth-send-email |
Hosted-only today; export hook config or redeploy from Supabase dashboard snapshot |
| Apple / Google EF secrets | Export from hosted Supabase secrets vault at T-2 |
| First Onset Docker ops | Reuse RealOutcomes scripts as template; pin .supabase-version |
| PHI decrypt | Mandatory smoke script (§7) |
| Option B fallback | Port critical EFs to Next.js routes only if Deno deps fail (+5–8 days); do not trigger unless proof fails |
4. Auth unification plan (clinic-first)¶
4.1 Steady-state target¶
| Surface | IdP | When |
|---|---|---|
| Clinic portal | https://auth.last1.id + @last1id/sdk |
T-3 (before clinic DNS cutover) |
| iOS App Store prod | Product GoTrue (hosted → R940) | T-5 (App Store release gate) |
| TestFlight | GoTrue on R940 VM 119 | T-4b |
4.2 OIDC client (onset)¶
Live state: ONSET_APP_SECRET already on last1id-app (per RealOutcomes SPRINT-2 audit). OAuth client app_id: onset registered on auth.last1.id.
T-3 S2-A tasks (mirror RealOutcomes):
| Item | Target |
|---|---|
| client_id | onset |
| redirect_uris | https://clinic.theonset.app/auth/callback, LAN dev http://192.168.1.101:3000/auth/callback |
| scopes | openid, email, profile, offline_access, identity:read, app_links:read, trust_events:write |
| BWS (Last1.id) | LAST1_ID_R940_PROD_ONSET_APP_SECRET |
| BWS (onset) | ONSET_APP_PROD_LAST1_OAUTH_*, ONSET_APP_PROD_LAST1_ID_ISSUER |
4.3 Clinic code changes (T-3)¶
| Repo | Files / work |
|---|---|
clinic.theonset.app |
Add @last1id/sdk, last1Auth.ts, OAuth callback route, replace Supabase password/Google login for practitioner flows |
| Server | JWKS validation (jose) replacing supabase.auth.getUser() on protected routes |
| Google OAuth | Moves to auth.last1.id (not product GoTrue) |
| MFA (TOTP) | Revalidate owner MFA via Last1 ID credentials or defer forced MFA to post-migration |
4.4 FK remap (Gate D)¶
32 FKs → auth.users.id. RealOutcomes lesson: id_token.sub must equal auth.users.id after remap.
T-3 remap scope (pre-user):
| User | Action |
|---|---|
curryrg@gmail.com (practitioner) |
Map to Last1 identity UUID |
hello@unicornryda.com (test) |
Map or recreate test practitioner row |
| Demo + promo expired | Low risk; preserve or orphan |
Practitioner–patient graph: Verify practitioners.user_id, shared_checkins.patient_app_user_id, encrypted practitioner_patients after remap.
4.5 Phased auth timeline¶
T-2: Gate B restores auth.users + GoTrue on VM 119
T-3: Clinic portal → auth.last1.id (GoTrue still serves iOS/TestFlight EF JWT paths)
T-4: Clinic DNS to R940 (clinic uses OIDC; iOS App Store still hosted Supabase)
T-4b: TestFlight → R940 Supabase + GoTrue
T-5: App Store → R940; retire GoTrue; optional full portfolio SSO for iOS via future sprint
4.6 Cross-stack window (critical)¶
After T-4, clinic and iOS App Store use different Postgres until App Store ships:
| Writer | Database | Risk |
|---|---|---|
| Clinic (R940) | VM 119 | Active |
| iOS App Store (hosted) | emxrmjys… |
Stale fork |
Mitigation (accepted): No external users; operator uses TestFlight against R940 after T-4b; dormant promo accounts unaffected.
5. Edge Function replatform strategy (24 vs RealOutcomes 8)¶
5.1 Function inventory¶
From onset-ios (20 deployed names):
| Function | JWT | Category | T-2 smoke priority |
|---|---|---|---|
daily-insight |
false | AI/cron | P2 |
admin-dashboard |
false | Ops | P3 |
generate-protocol |
true | AI | P1 |
client-event |
false | Telemetry | P2 |
scan-supplement-label, scan-supplement, search-supplement |
false | AI/Anthropic | P1 |
resolve-branded-product, popular-branded-products |
false | Catalog | P2 |
simulate-protocol-lab, scan-label, lab-intelligence, parse-lab |
mixed | Lab/PHI-adjacent | P1 |
apple-link, google-link, delete-account |
false | Auth helpers | P1 |
last1-link, last1-unlink, last1-connect-assertion |
false | Last1/LastVet bridge | P0 |
auth-send-email |
false | GoTrue hook | P1 (export) |
From clinic.theonset.app (4):
| Function | JWT | T-2 smoke |
|---|---|---|
practitioner-notifications |
true | P0 (PHI decrypt + SendGrid) |
protocol-import, protocol-extract, fitness-extract |
false | P1 |
5.2 Deployment approach¶
- Single compose on VM 119 — all 24 functions in
volumes/functions/<name>/. _shared/copied into each function dir or mounted per Supabase self-host pattern (match RealOutcomes T3-2).- Secrets: unified
.env.functionson VM 119 + BWS sync. - Smoke tiers: P0 must PASS for Gate B sign-off; P1 before T-4; P2/P3 before T-5.
- No systemd timers for Onset EFs (unlike RealOutcomes) — patient AI functions are on-demand; clinic crons use pg_cron → clinic HTTP, not EF cron.
5.3 Anthropic / AI path¶
Functions share _shared/anthropic_models.ts, ai_sanitize.ts. T-2 verify ANTHROPIC_API_KEY on stack; run one label scan + one lab parse smoke with test payload (no prod PHI in logs).
6. pg_cron migration plan¶
6.1 Architecture¶
Onset uses pg_cron + pg_net + vault to POST to clinic portal routes (not Edge Functions):
pg_cron → trigger_clinic_*() → trigger_clinic_cron_route(path)
→ vault: clinic_portal_base_url + clinic_cron_secret
→ net.http_post → https://clinic…/api/cron/…
Migration source: onset-ios/supabase/migrations/20260624121000_fullscript_hourly_cron.sql
6.2 Jobs (prod, restored via Gate B)¶
| jobname | Schedule (UTC) | Pacific | Clinic route |
|---|---|---|---|
clinic_protocol_deliveries |
0 * * * * |
top of hour | /api/cron/protocol-deliveries |
clinic_fullscript_sync |
15 * * * * |
:15 | /api/cron/fullscript-sync |
clinic_weekly_summary |
0 13 * * 1 |
Mon 06:00 PDT | /api/cron/weekly-summary |
6.3 T-2 / T-4 checklist¶
| Step | When |
|---|---|
| Restore jobs via Gate B dump | T-2 |
Verify vault.secrets present (clinic_cron_secret, clinic_portal_base_url) |
T-2 |
Point clinic_portal_base_url to R940 staging hostname; fire manual trigger; confirm clinic 200 |
T-2 |
| Pause or accept misfire during T-3 clinic auth work if staging URL unstable | T-3 |
Update vault to https://clinic.theonset.app + R940 CRON_SECRET |
T-4 (Sun window) |
| Confirm Monday weekly summary after first post-cutover week | Ryan spot-check |
T-4 alignment with cutover window: Complete vault update before Mon 06:00 PDT if cutover runs Sun 03:00–07:00 PDT.
7. Gate B rehearsal + PHI decrypt smoke test¶
7.1 Gate B (standard)¶
| Step | Acceptance |
|---|---|
pg_dump hosted emxrmjysvxeboavhatry (PG 17 client) |
Archive to backups/onset-gate-b-YYYY-MM-DD/ |
pg_restore to VM 119 |
Non-fatal auth/storage errors documented (RealOutcomes pattern) |
| Row parity | auth.users=8, practitioner_patients=5, product_events≈244, cron.job=3 |
| Extensions | pg_cron, pg_net, vault |
| Stack health | All compose services healthy |
7.2 PHI decrypt smoke (mandatory — Ryan Q6)¶
| Step | Acceptance |
|---|---|
Copy exact PATIENT_PII_ENCRYPTION_KEY from Railway prod to BWS + R940 clinic env + EF secret |
Byte-identical to hosted |
Run decrypt on all 5 practitioner_patients rows |
Plaintext email/name match expected operator/demo values |
Verify patient_email_hash unchanged |
SHA-256 stable |
| Clinic API smoke on R940 (staging hostname) | Practitioner patient list renders decrypted names |
practitioner-notifications EF |
Decrypt path returns email for test patient row |
Fail criteria: Any decrypt returns empty string → STOP T-2; do not proceed to T-3.
Script: scripts/onset/gate-b-phi-decrypt-smoke.sh (T-2 deliverable).
8. BWS project recommendation¶
| Option | Verdict |
|---|---|
| New Bitwarden project | Not needed if onset project exists |
Existing onset project |
Recommended — mirror RealOutcomes pattern |
Store clinic keys in realoutcomes |
Reject — wrong product boundary |
| Store clinic keys in Last1.id | Reject — only OIDC app secret cross-ref |
Ryan T-2 kickoff action: Confirm onset BWS project UUID (admin references ONSET_SUPABASE_* keys today — likely same project or HQ; consolidate under onset + ONSET_APP_PROD_*).
Add alongside legacy keys (delete at T-5): full inventory in ONSET_MIGRATION_PLAN.md § BWS.
LastVet identity assertion coordination¶
| Consumer | Secret env | BWS key |
|---|---|---|
Onset EF last1-connect-assertion |
ONSET_IDENTITY_ASSERTION_SECRET |
ONSET_APP_PROD_ONSET_IDENTITY_ASSERTION_SECRET (= platform shared) |
| LastVet API verifier | (existing) | LAST1_PLATFORM_SHARED_ONSET_IDENTITY_ASSERTION_SECRET |
T-3 gate: Rotate only if LastVet + Onset updated atomically; otherwise migrate unchanged like PHI key.
9. GitGuardian D-live map (onset-ios)¶
| ID | Detector | Action at T-2/T-5 |
|---|---|---|
| 29340466 | Supabase JWT prod xcconfig | New R940 anon key in TestFlight/App Store; RESOLVE at T-5 |
| 34103515 | Supabase JWT staging | RESOLVE when staging hosted deleted T-5 |
| 34103516 | Postgres docs | Close INVALID after doc scrub |
| 34103517 | Postgres docs | Close INVALID after doc scrub |
Expand at T-2: Railway GraphQL env audit for clinic-theonset-app (prod + staging services); Supabase dashboard secrets export (Anthropic, Apple, Google, SendGrid EF).
10. Staging decision¶
| Option | Verdict |
|---|---|
| Migrate hosted staging to separate R940 VMs | Defer — pre-user; not worth 2× infra |
Keep hosted Onset-staging until T-5 |
Accept — iOS staging xcconfig + clinic staging Railway |
| Tear down staging with prod at T-5 | Recommended |
11. Compressed sprint gates (Ryan pacing)¶
| Gate | Required | Soak compressed? |
|---|---|---|
| Gate B + PHI decrypt smoke | Yes | N/A |
| Clinic OIDC (T-3) | Yes | No multi-day soak; same-day Ryan login |
| Clinic DNS (T-4 Sun window) | Yes | No 48h soak |
| TestFlight (T-4b) | Yes | Operator same-day |
| App Store (T-5) | Yes | Wait for Apple review (external) |
| Hosted teardown (T-5) | Yes | Archive first |
12. Ryan decisions captured (T-1)¶
| # | Decision | Disposition |
|---|---|---|
| Q1 | Pre-user inventory final | Recorded §1 |
| Q2 | Sun 03:00–07:00 PDT cutover | Locked |
| Q3 | TestFlight → App Store | In plan T-4b/T-5 |
| Q4 | Clinic-first auth; GoTrue until App Store | §4 phased plan |
| Q5 | VMIDs 118/119 | §2 |
| Q6 | Migrate-as-is + Gate B PHI smoke | §7 |
| T-1 go | Authorized 2026-08-28 | This report |
13. STOP — Next gate¶
| Phase | Status | Ryan action |
|---|---|---|
| T-0 + sub-audits | Done | Approved |
| T-1 Planning | Done | Approve T-2 kickoff |
| T-2 VMs + Gate B + PHI smoke | Blocked | Authorize provision 118/119 |
| T-3 Clinic OIDC + app deploy | Blocked | — |
| T-4 onset-edge + DNS | Blocked | Sun window |
| T-4b TestFlight | Blocked | — |
| T-5 App Store + teardown | Blocked | — |
T-2 will: provision VMs 118/119, deploy Supabase Docker, Gate B dump/restore, PHI decrypt smoke PASS, populate BWS ONSET_APP_PROD_*, Edge Function P0 smoke — without clinic DNS cutover or hosted teardown.