Skip to content

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)

  1. Provision realoutcomes-supabase (VMID 117, LAN 192.168.1.98).
  2. Install official Supabase Docker Compose (supabase/docker pinned release via .supabase-version).
  3. Configure:
  4. Postgres 17 data on ZFS dataset (tank-fast, 150 GB).
  5. Disable Realtime (unused in repo audit).
  6. Studio: Tailscale/LAN only, not public.
  7. Storage: local filesystem backend (uploads bucket; 0 objects today).
  8. Copy supabase/functions/* → volumes/functions/; .env.functions for OpenAI, Firecrawl, SendGrid.
  9. Gate B: pg_dump from hosted cpocwrygxlqhzvmlrpjv → restore into R940 Postgres.
  10. Replay 5 pg_cron jobs (export from hosted cron.job; see T3-0 audit).
  11. 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
  1. Verify extensions: pg_cron, pg_trgm, pgcrypto, uuid-ossp.
  2. 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)

  1. Provision realoutcomes-r940 (VMID 116, LAN 192.168.1.99).
  2. Clone/build Last-1-Enterprises/RealOutcomes Docker image on VM.
  3. Render /opt/realoutcomes/.env from BWS:
  4. SUPABASE_URL=http://192.168.1.98:8000 (Kong on supabase VM)
  5. DATABASE_URL=postgresql://...@192.168.1.98:5432/postgres (LAN; match last1.id pattern)
  6. Rotated Stripe/SendGrid/Firecrawl/OpenAI keys
  7. PUBLIC_URL=https://realoutcomes-r940.last1.enterprises (staging hostname, no prod DNS yet)
  8. Bind container to LAN :5000 (lesson from admin T3-3).
  9. systemd timer: monthly-irs-sync (0 3 1 * *).
  10. Staging tunnel route on lastvet-edge (parallel soak, DNS not cut over).
  11. Smoke: directory API, login, admin gateway via Railway admin.

T3-4 — Integration verification

  1. Trigger test crawl → score pipeline on R940.
  2. Confirm pg_cron job run details + Edge Function logs.
  3. Stripe CLI test webhook → R940 origin.
  4. SendGrid test (alert digest dry run).
  5. Ryan browser checklist (from T3-0 verification matrix).

T3-5 — DNS cutover

  1. Update Stripe webhook URL to https://realoutcomes.app/api/stripe/webhook.
  2. Cloudflare: apex + www → tunnel → VM 116 :5000.
  3. Update GoTrue SITE_URL / redirect allow-list for prod domain.
  4. 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

  1. Archive Railway env + hosted Supabase metadata to backups/realoutcomes-railway-teardown-YYYY-MM-DD/.
  2. Delete Railway services RealOutcomes.app, monthly-irs-sync.
  3. Delete hosted Supabase project cpocwrygxlqhzvmlrpjv (after final Gate B proof).
  4. Remove legacy unprefixed BWS keys; close GG D3 incidents 29169495–29169500.
  5. 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