SPRINT-3 Execution Plan — realoutcomes-edge + Production Cutover¶
Status: Plan for Ryan review. No execution.
Date: 2026-08-28
Prerequisite: SPRINT-2 CLOSED (Gate F)
Principles: R940_PRODUCT_ISOLATION_PRINCIPLES_v1.md
Executive summary¶
SPRINT-3 puts production traffic on R940 RealOutcomes by creating a dedicated Cloudflare Tunnel (realoutcomes-edge) on VM 116 (192.168.1.99) and cutting realoutcomes.app DNS to it.
Sprint mode: production cutover directly. No staging hostname on lastvet-edge. No resurrecting T3-4 scripts that added RealOutcomes routes to lastvet-edge or public Supabase Kong for GoTrue (obsolete after SPRINT-2).
Auth is already unified on auth.last1.id (SPRINT-2). SPRINT-3 is ingress + PUBLIC_URL alignment, not auth rework.
Target architecture (steady state after SPRINT-3)¶
Browser ──► Cloudflare (proxied)
│
│ realoutcomes.app CNAME → realoutcomes-edge tunnel
▼
VM 116 realoutcomes-app :5000 (Express monolith)
│
├── OAuth ──► auth.last1.id (VM 114 / last1id-edge) [already live]
│
└── DATABASE_URL ──► hosted Supabase (until SPRINT-5 DB cutover)
optional future: VM 117 Postgres via LAN
VM 117 realoutcomes-db :8000 (Envoy)
│
├── Edge Function timers (systemd → LAN :8000/functions/v1)
├── Storage (LAN)
└── Postgres + pg_cron (LAN)
└── **No public tunnel required for SPRINT-3** (see §4)
Phase map and Ryan gates¶
| Phase | Work | Ryan gate |
|---|---|---|
| S3-1 | Create Cloudflare tunnel realoutcomes-edge + credentials |
Review tunnel UUID |
| S3-2 | Install cloudflared on VM 116 (realoutcomes-app) |
— |
| S3-3 | Ingress: realoutcomes.app (+ www if used) → http://127.0.0.1:5000 |
Review config before DNS |
| S3-4 | Update app env: PUBLIC_URL, OAuth redirect URIs, Stripe webhook URL prep |
Approve env diff |
| S3-5 | Ryan DNS: apex realoutcomes.app CNAME → {tunnel-id}.cfargotunnel.com (proxied) |
Required before traffic |
| S3-6 | PF checks + automated smoke | Cursor report |
| S3-7 | Ryan browser E2E on prod hostname | Gate S3-R — PASS before SPRINT-4 |
| S3-8 | Deprecate Railway origin traffic (keep rollback net briefly) | Ryan sign-off |
Cursor will not change Cloudflare DNS. Ryan performs DNS (same pattern as SPRINT-1).
S3-1 — Cloudflare tunnel creation¶
| Item | Target |
|---|---|
| Tunnel name | realoutcomes-edge |
| Connector host | VM 116 realoutcomes-app (.99) |
| Credentials | Store in BWS: REALOUTCOMES_APP_EDGE_CLOUDFLARED_TUNNEL_CREDENTIALS |
| CNAME target | {tunnel-uuid}.cfargotunnel.com |
Script (to add): scripts/realoutcomes/install-realoutcomes-edge-cloudflared.sh (mirror scripts/last1-id/install-last1id-edge-cloudflared.sh).
S3-2 / S3-3 — cloudflared on VM 116¶
Install location: VM 116 only (Option A per isolation principles).
Proposed /etc/cloudflared/config.yml:
tunnel: realoutcomes-edge
credentials-file: /etc/cloudflared/{TUNNEL_UUID}.json
ingress:
- hostname: realoutcomes.app
service: http://127.0.0.1:5000
- hostname: www.realoutcomes.app
service: http://127.0.0.1:5000
- service: http_status:404
Validate before DNS:
cloudflared tunnel ingress validate
curl -sf -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5000/
PF checks (to add): scripts/realoutcomes/verify-realoutcomes-edge.sh (mirror last1id PF pattern).
S3-4 — App configuration updates (VM 116)¶
| Variable | Current (typical) | After SPRINT-3 |
|---|---|---|
PUBLIC_URL |
http://192.168.1.99:5000 or staging |
https://realoutcomes.app |
LAST1_OAUTH_REDIRECT_URI |
LAN callback | https://realoutcomes.app/auth/callback |
app_clients.redirect_uris (auth.last1.id) |
includes LAN | Add prod URI; remove LAN after Ryan confirms prod-only dev workflow |
| Stripe webhook | Railway URL | https://realoutcomes.app/api/stripe/webhook (update in Stripe dashboard at cutover) |
Deploy: bash /opt/realoutcomes/deploy-realoutcomes-app.sh after env render from BWS.
Do not add RealOutcomes hostnames to lastvet-edge.
S3-5 — Ryan DNS action¶
| Record | Type | Target | Proxy |
|---|---|---|---|
realoutcomes.app (apex) |
CNAME | {TUNNEL_UUID}.cfargotunnel.com |
Proxied (orange cloud) |
www.realoutcomes.app (if used) |
CNAME | same | Proxied |
Note: Cloudflare apex CNAME flattening applies. Confirm SSL mode Full (strict) once origin serves HTTPS via tunnel (tunnel terminates TLS at edge).
Rollback: repoint DNS to Railway CNAME/target documented in pre-cutover snapshot; tunnel stays up but unused.
S3-6 — Automated smoke (Cursor)¶
| Test | Pass criteria |
|---|---|
GET https://realoutcomes.app/ |
200 (after DNS) |
GET /api/auth/user (no bearer) |
401 |
GET /api/directory?limit=1 |
200 |
GET https://auth.last1.id/.well-known/openid-configuration |
200 |
OAuth start POST /api/auth/last1/start |
Redirect to auth.last1.id with client_id=realoutcomes |
VM 117 POST /functions/v1/process-score-runs (LAN, service role) |
200 |
VM 117 GET /storage/v1/status (LAN) |
200 |
supabase-auth container |
Absent |
§4 — VM 117 Supabase Kong: public exposure decision¶
Question¶
After auth unification, is a public Supabase Kong hostname (realoutcomes-db… / tunnel to 192.168.1.98:8000) still required?
Recommendation: LAN-only sufficient for SPRINT-3¶
| Path | Needs public Kong? |
|---|---|
| User login | No — auth.last1.id only (SPRINT-2 verified) |
| App API / Drizzle | No — server uses DATABASE_URL directly |
| Edge Function schedules | No — systemd on VM 117 curls 127.0.0.1:8000/functions/v1 |
Browser @supabase/supabase-js auth |
N/A — retired with GoTrue |
| Browser-direct Storage upload | Audit before S3 — if client uploads via Supabase JS to Storage API, either (a) proxy through Express, or (b) add optional second ingress hostname on realoutcomes-edge pointing to VM 117 :8000 storage routes only |
T3-4 staging plan (configure-tunnel-staging-db.sh, GoTrue redirects) is obsolete. Do not execute.
Default SPRINT-3 scope: app hostname only. Revisit public Kong only if SPRINT-4 browser E2E finds a client-side Storage/Functions call that cannot use server proxy.
Security posture: VM 117 Postgres/Envoy remain LAN + UFW (app VM .99 → .98:8000/5432 already provisioned). No product routes on lastvet-edge.
Obsolete artifacts (do not run)¶
| Script / doc | Reason |
|---|---|
configure-tunnel-staging-app.sh (lastvet-edge) |
Violates product isolation; superseded by realoutcomes-edge |
configure-tunnel-staging-db.sh |
Was for browser GoTrue/Kong; GoTrue retired |
configure-gotrue-staging-redirects.sh |
GoTrue retired |
| T3-4 § Ryan R-2/R-3 (Supabase public + GoTrue allow-list) | Superseded |
Rollback¶
| Point | Action |
|---|---|
| After DNS cutover, before Railway delete | Repoint realoutcomes.app DNS to Railway; revert Stripe webhook |
| Tunnel misconfig | Stop cloudflared on VM 116; DNS unchanged |
| Auth regression | Restore VM 117 GoTrue from Gate E backup — only if Last1 ID path breaks; not expected |
Pre-cutover snapshot: Railway DNS target + Stripe webhook URL + BWS PUBLIC_URL values.
SPRINT-3 vs Tier 3 phase mapping¶
| Legacy Tier 3 | SPRINT-3 equivalent |
|---|---|
| T3-4 staging tunnel on lastvet-edge | Replaced — production realoutcomes-edge |
| T3-5 DNS cutover | SPRINT-3 S3-5 (direct prod, no staging) |
| T3-6 soak | SPRINT-4 E2E + monitoring |
| T3-7 teardown | SPRINT-5 |
Ryan authorization¶
STOP. Review this plan and reply:
SPRINT-3 go— authorize Cursor to execute S3-1..S3-4 and prepare PF scripts; Ryan performs S3-5 DNS when prompted.SPRINT-3 go through S3-4 only— tunnel + config without DNS.- Adjustments — specify (e.g. include
www, defer Stripe webhook, require Storage audit first).
References¶
- SPRINT-2_GATE_F_REPORT.md
- R940_PRODUCT_ISOLATION_PRINCIPLES_v1.md
- SPRINT-1_LAST1ID_EDGE_REPORT.md
scripts/last1-id/install-last1id-edge-cloudflared.sh
END SPRINT-3 plan