RealOutcomes Migration Plan — R940 Full-Stack Sovereignty
Status: Planning (T3-1). No execution until Ryan approves T3-2.
Parent audit: TIER_3_REALOUTCOMES_MIGRATION_AUDIT.md
T3-1 report: TIER_3_REALOUTCOMES_T3-1_REPORT.md
Sequencing: TIER_RESEQUENCING_2026-08-28.md
Scope lock (Ryan decisions 2026-08-28)
| Decision |
Choice |
| Edge Functions |
Option A: self-hosted Supabase full stack on R940 |
| Marketing split |
Defer post T3-7 (separate work item) |
| D-live rotation |
Yes during REALOUTCOMES_R940_* BWS setup |
| Auth model |
Keep product-local Supabase Auth for Tier 3; defer auth.last1.id unification |
Target architecture
Cloudflare DNS (T3-5)
│
lastvet-edge tunnel
│
┌────────────┴────────────┐
│ VM 116 realoutcomes-r940 │
│ Express monolith :5000 │
│ IRS monthly systemd timer│
└────────────┬────────────┘
│ LAN
┌────────────┴────────────┐
│ VM 117 realoutcomes-supabase │
│ Supabase Docker (Kong :8000)│
│ Postgres 17 + pg_cron │
│ GoTrue · Storage · Edge RT │
│ 8 Edge Functions │
└─────────────────────────────┘
Downstream unchanged until Tier 6: last1-admin proxy → REALOUTCOMES_API_BASE + ADMIN_GATEWAY_SECRET.
Phase map
| Phase |
Goal |
Exit criteria |
| T3-0 |
Pre-migration audit |
Audit approved |
| T3-1 |
Planning + BWS layout + VMIDs |
This plan + T3-1 report approved |
| T3-2 |
Supabase VM + Gate B dump/restore rehearsal |
R940 DB matches hosted row counts; restore drill documented |
| T3-3 |
App VM + Docker deploy + parallel soak |
R940 app serves traffic on staging hostname; Railway still prod |
| T3-4 |
Edge Functions + pg_cron + IRS cron + Stripe test |
Crawl/score/alert pipeline verified; webhook test event 200 |
| T3-5 |
DNS cutover realoutcomes.app |
Apex on R940; rollback tested |
| T3-6 |
Soak 48–72 h |
No P1 alerts; admin proxy OK |
| T3-7 |
Teardown + GG close + runbook |
Railway + hosted Supabase deleted; muster closed |
Calendar: ~5–8 working days after T3-2 sign-off.
T3-2 — Database + Supabase stack (VM 117)
- Provision
realoutcomes-supabase (VMID 117, LAN 192.168.1.98).
- Install official Supabase Docker Compose (
supabase/docker pinned release via .supabase-version).
- Configure:
- Postgres 17 data on ZFS dataset (
tank-fast, 150 GB).
- Disable Realtime (unused in repo audit).
- Studio: Tailscale/LAN only, not public.
- Storage: local filesystem backend (
uploads bucket; 0 objects today).
- Copy
supabase/functions/* → volumes/functions/; .env.functions for OpenAI, Firecrawl, SendGrid.
- Gate B:
pg_dump from hosted cpocwrygxlqhzvmlrpjv → restore into R940 Postgres.
- Replay 5 pg_cron jobs (export from hosted
cron.job; see T3-0 audit).
- Create systemd timers on VM 117 for Edge Function schedules (hosted dashboard cron → self-hosted curl invocations):
| Function |
Suggested schedule (confirm vs hosted dashboard in T3-2) |
scheduled-recrawl |
Weekly (align with pg_cron job 18) |
process-score-runs |
Daily or post-crawl |
process-status-changes |
Daily |
run-validations |
Daily |
send-alert-digests |
Daily |
- Verify extensions:
pg_cron, pg_trgm, pgcrypto, uuid-ossp.
- Generate new JWT/anon/service keys; store in BWS
REALOUTCOMES_R940_PROD_* (do not reuse hosted keys post-cutover).
T3-3 — App VM + parallel soak (VM 116)
- Provision
realoutcomes-r940 (VMID 116, LAN 192.168.1.99).
- Clone/build
Last-1-Enterprises/RealOutcomes Docker image on VM.
- Render
/opt/realoutcomes/.env from BWS:
SUPABASE_URL=http://192.168.1.98:8000 (Kong on supabase VM)
DATABASE_URL=postgresql://...@192.168.1.98:5432/postgres (LAN; match last1.id pattern)
- Rotated Stripe/SendGrid/Firecrawl/OpenAI keys
PUBLIC_URL=https://realoutcomes-r940.last1.enterprises (staging hostname, no prod DNS yet)
- Bind container to LAN
:5000 (lesson from admin T3-3).
- systemd timer:
monthly-irs-sync (0 3 1 * *).
- Staging tunnel route on
lastvet-edge (parallel soak, DNS not cut over).
- Smoke: directory API, login, admin gateway via Railway admin.
T3-4 — Integration verification
- Trigger test crawl → score pipeline on R940.
- Confirm pg_cron job run details + Edge Function logs.
- Stripe CLI test webhook → R940 origin.
- SendGrid test (alert digest dry run).
- Ryan browser checklist (from T3-0 verification matrix).
T3-5 — DNS cutover
- Update Stripe webhook URL to
https://realoutcomes.app/api/stripe/webhook.
- Cloudflare: apex + www → tunnel → VM 116
:5000.
- Update GoTrue
SITE_URL / redirect allow-list for prod domain.
- Keep Railway + hosted Supabase as rollback net (no traffic).
T3-6 — Soak (48–72 h)
- Cron probes (health + auth sample).
- Prometheus alerts on VMs 116/117 (node_exporter).
- Monitor crawl queue, IRS import window if in soak period.
T3-7 — Teardown
- Archive Railway env + hosted Supabase metadata to
backups/realoutcomes-railway-teardown-YYYY-MM-DD/.
- Delete Railway services
RealOutcomes.app, monthly-irs-sync.
- Delete hosted Supabase project
cpocwrygxlqhzvmlrpjv (after final Gate B proof).
- Remove legacy unprefixed BWS keys; close GG D3 incidents 29169495–29169500.
- Publish
REALOUTCOMES_MIGRATION_RUNBOOK.md; close muster item.
BWS key inventory (target)
Project: realoutcomes (788ed6ec-0284-4bbc-b57c-b4840004688e)
Prefix: REALOUTCOMES_R940_PROD_*
| Key |
Source |
REALOUTCOMES_R940_PROD_DATABASE_URL |
Generated at stack init (LAN IP) |
REALOUTCOMES_R940_PROD_SUPABASE_URL |
http://192.168.1.98:8000 internal |
REALOUTCOMES_R940_PROD_SUPABASE_ANON_KEY |
Generated at cutover |
REALOUTCOMES_R940_PROD_SUPABASE_SERVICE_ROLE_KEY |
Generated at cutover |
REALOUTCOMES_R940_PROD_JWT_SECRET |
Generated at stack init |
REALOUTCOMES_R940_PROD_SESSION_SECRET |
Railway sync → rotate |
REALOUTCOMES_R940_PROD_ADMIN_GATEWAY_SECRET |
Railway sync (do not rotate without admin Tier 6 coord) |
REALOUTCOMES_R940_PROD_STRIPE_* |
Railway sync → rotate webhook (GG 29169495) |
REALOUTCOMES_R940_PROD_SENDGRID_* |
Railway sync → rotate (GG 29169499) |
REALOUTCOMES_R940_PROD_OPENAI_API_KEY |
Railway sync → reissue if used (GG 29169496 invalid) |
REALOUTCOMES_R940_PROD_FIRECRAWL_API_KEY |
Railway sync → rotate (GG 29169500) |
REALOUTCOMES_R940_PROD_FEC_API_KEY |
Railway sync |
REALOUTCOMES_R940_PROD_PUBLIC_URL |
Staging then prod override |
REALOUTCOMES_R940_PROD_VITE_SUPABASE_* |
Build-time; match server anon/url |
Sync script (T3-2): scripts/realoutcomes/sync-r940-bws-from-railway.py (to be added at T3-2 kickoff).
Rollback
| Point |
Action |
| Before T3-5 |
Stop R940 containers; no user impact |
| After T3-5 |
Repoint DNS to Railway; revert Stripe webhook |
| After T3-7 |
Irreversible — requires Gate B archive |
Deferred work (out of Tier 3 scope)
| Item |
When |
| Marketing → Cloudflare Pages split |
Post T3-7 (realoutcomes-marketing-pages-split) |
Universal SSO via auth.last1.id |
Separate identity unification workstream |
last1-admin REALOUTCOMES_API_BASE → R940 |
Tier 6 admin reconnect |