Skip to content

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:

  1. Confirm trigger functions exist on VM 119 (restored Gate B).
  2. Update vault: clinic_portal_base_url → https://clinic.theonset.app; clinic_cron_secret → matches R940 ONSET_APP_PROD_CRON_SECRET.
  3. Refactor trigger_clinic_weekly_summary() to use vault clinic_portal_base_url (today hardcodes prod URL — OK after DNS, but unify with other crons).
  4. Run cron.schedule(...) as superuser (see sub-audit SQL) or ALTER TABLE cron.job OWNER TO postgres + targeted pg_restore --data-only -n cron -t job from Gate B archive.
  5. 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.job count → 3
  • [ ] storage.objects count → 20
  • [ ] Vault secrets updated for R940 clinic base URL + cron secret
  • [ ] /patients/new works 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)

  1. TestFlight build with R940 SUPABASE_URL + R940 anon key.
  2. Operator smoke: Apple Sign-In (proven Gate E), check-in, protocol view.
  3. 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


END T-4 execution plan