T-4 Execution Plan — onset-edge + Clinic DNS Cutover¶
Status: Plan for Ryan review. No execution.
Date: 2026-08-30
Prerequisite: T-3 CLOSED (Gate F)
Principles: R940_PRODUCT_ISOLATION_PRINCIPLES_v1.md, ONSET_MIGRATION_PLAN.md
Executive summary¶
T-4 puts clinic production traffic on R940 by creating a dedicated Cloudflare Tunnel (onset-edge) on VM 118 (192.168.1.101) and cutting clinic.theonset.app DNS to it.
T-3 is done: clinic auth is auth.last1.id only. T-4 is ingress + Gate B gap closure + vault/cron alignment, not auth rework.
iOS App Store and hosted Supabase remain the rollback net through T-4 and T-4b. Do not update Release xcconfig or tear down hosted at T-4.
Cutover window: Sun 03:00–07:00 PDT (Ryan locked in T-1).
Target architecture (steady state after T-4)¶
Browser ──► Cloudflare (proxied)
│
│ clinic.theonset.app CNAME → onset-edge tunnel
▼
VM 118 onset-app :3000 (Next.js clinic)
│
├── OAuth ──► auth.last1.id (last1id-edge) [live]
│
└── Supabase client ──► LAN http://192.168.1.102:8000
│
▼
VM 119 onset-db :8000 (Kong + GoTrue + EF + Postgres)
│
├── pg_cron → clinic /api/cron/* (after T4-0)
├── Storage (20 objects after T4-0)
└── GoTrue (iOS until T-5)
iOS App Store Release ──► hosted emxrmjys… (unchanged at T-4)
TestFlight R940 path ──► T-4b (optional public Kong hostname or Tailscale)
Phase map and Ryan gates¶
| Phase | Work | Ryan gate |
|---|---|---|
| T4-0 | Pre-cutover fixes (cron, storage, /patients/new) |
T4-0 sign-off before tunnel/DNS |
| T4-1 | Create Cloudflare tunnel onset-edge + BWS credentials |
Review tunnel UUID |
| T4-2 | Install cloudflared on VM 118; ingress config |
Review before DNS |
| T4-3 | Vault + app env: clinic URL, cron secret, webhook URL prep | Approve env diff |
| T4-4 | Ryan DNS: clinic.theonset.app CNAME → {tunnel-id}.cfargotunnel.com |
Sun window — required |
| T4-5 | Automated smoke + cron manual trigger | Cursor report |
| T4-6 | Ryan clinic E2E on prod hostname | Gate T4-R — PASS before T-4b |
| T4-7 | Document rollback net (Railway + hosted still live) | Informational |
Cursor will not change Cloudflare DNS. Ryan performs DNS (RealOutcomes / Last1 ID pattern).
Authorization: Reply T-4 go to start T4-0. Do not install tunnel or touch DNS without explicit go.
T4-0 — Pre-cutover fixes (MUST complete before DNS)¶
Source: TIER_4_ONSET_T-3_SUBAUDITS.md, Gate E backlog.
T4-0a — Restore pg_cron (3 jobs)¶
Current: VM 119 SELECT COUNT(*) FROM cron.job → 0
Target: 3 jobs matching hosted prod
| jobname | Schedule (UTC) | Function |
|---|---|---|
clinic_weekly_summary |
0 13 * * 1 |
trigger_clinic_weekly_summary() |
clinic_protocol_deliveries |
0 * * * * |
trigger_clinic_protocol_deliveries() |
clinic_fullscript_sync |
15 * * * * |
trigger_clinic_fullscript_sync() |
Steps:
- Confirm trigger functions exist on VM 119 (restored Gate B).
- Update vault:
clinic_portal_base_url→https://clinic.theonset.app;clinic_cron_secret→ matches R940ONSET_APP_PROD_CRON_SECRET. - Refactor
trigger_clinic_weekly_summary()to use vaultclinic_portal_base_url(today hardcodes prod URL — OK after DNS, but unify with other crons). - Run
cron.schedule(...)as superuser (see sub-audit SQL) orALTER TABLE cron.job OWNER TO postgres+ targetedpg_restore --data-only -n cron -t jobfrom Gate B archive. - Manual smoke:
SELECT trigger_clinic_protocol_deliveries();→ HTTP 200 to clinic cron route (against R940 LAN hostname pre-DNS, then prod hostname post-DNS).
Exit: SELECT COUNT(*) FROM cron.job → 3; manual trigger returns success.
T4-0b — Restore storage.objects (20 objects)¶
Current: VM 119 SELECT COUNT(*) FROM storage.objects → 0
Target: 20 objects (~3.2 MB) across 2 buckets
| Bucket | Public | Objects | Notes |
|---|---|---|---|
clinic-support-attachments |
No | 2 | Operator test screenshots |
templates |
Yes | 18 | Social/n8n Onset Daily Publisher assets |
Approaches (pick one at T4-0 gate):
| Approach | When | Notes |
|---|---|---|
| A. Schema align + metadata restore | Preferred | Match self-hosted storage columns to hosted; re-run Gate B object rows from dump |
| B. API file migration | Fallback | Download from hosted S3/public URLs → upload to R940 storage backend (~3.2 MB) |
Exit: SELECT COUNT(*) FROM storage.objects → 20; spot-check 1 public + 1 private URL via Kong.
T4-0c — Migrate /patients/new bootstrap off GoTrue¶
Gap: (portal)/patients/new/page.tsx calls supabase.auth.getUser() — only active clinic browser GoTrue call. Last1 session cookie does not populate GoTrue client session.
Fix (minimal): Server component or API route loads practitioner from getClinicSessionFromCookies() / service role; pass props to client. Mirror pattern used elsewhere post–Gate B.
Exit: Authenticated Ryan (curryrg@gmail.com) loads /patients/new after Last1 login without GoTrue client call.
T4-0 verification checklist¶
- [ ]
cron.jobcount → 3 - [ ]
storage.objectscount → 20 - [ ] Vault secrets updated for R940 clinic base URL + cron secret
- [ ]
/patients/newworks with Last1 session only - [ ] Manual cron trigger → 200 (protocol-deliveries minimum)
T4-1 — Cloudflare tunnel creation¶
| Item | Target |
|---|---|
| Tunnel name | onset-edge |
| Connector host | VM 118 onset-app (192.168.1.101) |
| BWS key | ONSET_APP_PROD_EDGE_CLOUDFLARED_TUNNEL_CREDENTIALS (or split ID + credentials per RealOutcomes pattern) |
| CNAME target | {tunnel-uuid}.cfargotunnel.com |
Script (to add): scripts/onset/install-onset-edge-cloudflared.sh (mirror scripts/realoutcomes/install-realoutcomes-edge-cloudflared.sh).
Do not add Onset routes to lastvet-edge, last1id-edge, or realoutcomes-edge.
T4-2 — cloudflared on VM 118¶
Proposed /etc/cloudflared/config.yml:
tunnel: <ONSET_APP_PROD_EDGE_CLOUDFLARED_TUNNEL_ID>
credentials-file: /etc/cloudflared/onset-edge.json
ingress:
- hostname: clinic.theonset.app
service: http://127.0.0.1:3000
- service: http_status:404
Optional (T-4b decision, not T-4 default): second hostname for public Kong :8000 if TestFlight cannot use Tailscale Serve.
T4-3 — Environment + vault alignment¶
| Secret / env | T-4 target |
|---|---|
ONSET_APP_PROD_PUBLIC_APP_URL |
https://clinic.theonset.app |
clinic_portal_base_url (vault VM 119) |
https://clinic.theonset.app |
clinic_cron_secret (vault) |
Matches ONSET_APP_PROD_CRON_SECRET on VM 118 |
| Stripe webhook | Update to R940 clinic URL if rotating at cutover |
| Fullscript webhook | Same |
| Last1 OAuth redirect | Confirm https://clinic.theonset.app/... registered |
Do not change: iOS Production.xcconfig, Staging.xcconfig (hosted), onset-web CF Pages prod Supabase env.
T4-4 — DNS cutover (Ryan only)¶
| Record | Action |
|---|---|
clinic.theonset.app |
CNAME → {tunnel-uuid}.cfargotunnel.com (proxied) |
Window: Sun 03:00–07:00 PDT
Rollback: Repoint CNAME to Railway origin; revert vault URLs if needed (ONSET_MIGRATION_PLAN.md § Rollback).
T4-5 — Automated verification¶
| Check | Expected |
|---|---|
GET https://clinic.theonset.app/login |
200 |
| Last1 OAuth redirect | Reaches auth.last1.id |
GET https://auth.last1.id/.well-known/openid-configuration |
200 |
VM 119 /auth/v1/health + apikey |
200 (GoTrue still up) |
| Edge smoke (sample) | practitioner-notifications, last1-connect-assertion reachable |
| Cron manual trigger post-DNS | HTTP 200 |
Script (extend): scripts/onset/gate-e-verify-partial-gotrue.sh → T-4 clinic hostname variant (to add).
T4-6 — Ryan manual verification (Gate T4-R)¶
| # | Test | Expected |
|---|---|---|
| T4-R-1 | Clinic login curryrg@gmail.com on prod DNS |
Dashboard loads |
| T4-R-2 | /patients/new authenticated |
Practitioner context loads (T4-0c fix) |
| T4-R-3 | Cron route reachable | Manual trigger or wait for hourly job |
| T4-R-4 | iOS App Store build still works on hosted | No regression (rollback net) |
| T4-R-5 | docker ps VM 119 |
supabase-auth healthy |
Reply T-4 pass or share failure detail.
T4-7 — Rollback net (keep until T-5)¶
| System | T-4 action |
|---|---|
| Railway clinic origin | Keep running — no traffic after DNS, rollback target |
| Hosted Supabase prod | Keep running — iOS App Store dependency |
| Hosted Supabase staging | Keep running — tear down at T-5 with prod |
| VM 119 GoTrue | Keep running — iOS path until T-5 |
Post-T-4: T-4b preview (not in T-4 scope)¶
- TestFlight build with R940
SUPABASE_URL+ R940 anon key. - Operator smoke: Apple Sign-In (proven Gate E), check-in, protocol view.
- Gate: TestFlight PASS before App Store submission (T-5).
Gate E already proved R940 Apple auth; T-4b is channel verification on prod TestFlight train.
T-3 backlog (post-T-4 hygiene)¶
Not blocking T4-R if /patients/new is fixed in T4-0c.
| Item | File | Notes |
|---|---|---|
| notes-editor anon RLS risk | notes-editor.tsx |
Migrate to service-role API routes |
| template-actions anon RLS risk | template-actions.tsx |
Same |
| Deferred signup | api/clinic/register/route.ts |
When product re-enables signup |
| Dead MFA code | lib/clinic-mfa.ts |
Remove unused module |
Related documents¶
- TIER_4_ONSET_T3_GATE_F_REPORT.md
- TIER_4_ONSET_T-3_SUBAUDITS.md
- SPRINT-3_REALOUTCOMES_EDGE_EXECUTION_PLAN.md (pattern reference)
END T-4 execution plan