Tier 3 T3-1 Report — RealOutcomes Planning + BWS + VMIDs¶
Date: 2026-08-28
Status: T3-1 COMPLETE. STOP here. Await Ryan authorization for T3-2 (VM provision + Gate B).
Parent audit: TIER_3_REALOUTCOMES_MIGRATION_AUDIT.md (T3-0 approved)
Execution plan: REALOUTCOMES_MIGRATION_PLAN.md
Executive summary¶
T3-1 confirms Option A (self-hosted Supabase full stack) is feasible on R940 with known gaps (Edge Function cron replatforming, first portfolio Supabase Docker ops). Recommended two-VM layout matching last1.id split: VM 117 hosts Supabase platform + Postgres; VM 116 hosts Express monolith + IRS cron.
Auth unification with auth.last1.id: Defer to a separate workstream (estimated 2–4 days, not ~1 day). Tier 3 keeps product-local GoTrue migrated via auth.users dump.
Marketing split: Deferred post T3-7 per Ryan decision.
BWS: Use existing realoutcomes project with new REALOUTCOMES_R940_PROD_* keys; D-live rotations scheduled during T3-2 BWS population.
1. Self-hosted Supabase feasibility (Option A)¶
Verdict: GO, with T3-2 proof tasks¶
Official Supabase Docker Compose (supabase/supabase/docker) ships Edge Runtime, Storage, GoTrue, PostgREST, Kong, pg_cron-capable Postgres, and documents function deployment to volumes/functions/. This matches RealOutcomes' hosted surface.
| Capability | Feasibility | Notes |
|---|---|---|
| Edge Runtime (8 functions) | Yes | Standard Deno functions in repo; copy to volumes/functions/; restart/recreate functions service. All use verify_jwt = false + service role (same trust model). |
Storage (uploads) |
Yes | 0 objects in hosted bucket today. Local filesystem backend sufficient; optional MinIO/S3 later. App uses server-side signed URLs via service role (server/fileStorage.ts). |
| GoTrue Auth | Yes | Lift auth.users (3 rows) in Gate B dump. Configure Google OAuth on self-hosted GoTrue (separate GCP client or defer Google until post-cutover). |
| pg_cron (5 jobs) | Yes | Extension already enabled on hosted DB. Supabase Postgres image supports pg_cron; replay job SQL from hosted export (T3-0 captured schedules). |
| Postgres 17 + pg_trgm | Yes | Hosted on 17.6.1; ~1.3 GB / ~2M IRS rows. Plan 150 GB ZFS data disk. |
| Realtime | N/A | No client Realtime usage found in repo. Disable in compose to save RAM. |
| RLS | Low risk | All 40 public tables use service_role-only policies; app uses Drizzle + direct Postgres (not PostgREST user JWT). No auth.uid() policies. |
Gaps before committing (T3-2 must prove)¶
| Gap | Mitigation |
|---|---|
| Edge Function schedules | Hosted uses Supabase Dashboard cron (not in repo). Repo comment: "scheduled functions, not pg_cron HTTP calls" (migrations/20260217_status_change_alerts.sql). T3-2: export hosted schedules from dashboard; implement systemd timers on VM 117 curling http://127.0.0.1:8000/functions/v1/<name> with service role Bearer. |
| First Supabase Docker ops on R940 | last1.id used custom GoTrue/Kong/bare Postgres, not official compose. Pin release via .supabase-version; document run.sh upgrade path. |
| Function secrets | compute-score (OpenAI), run-crawl (Firecrawl), send-alert-digests (SendGrid) need .env.functions / compose env on VM 117. |
| Resource sizing | Supabase docs: 8 GB+ RAM recommended for full stack. co-locate only platform on VM 117; keep app on VM 116. |
| Network | App VM reaches Supabase via LAN IP (Tailscale east-west Postgres blocked per portfolio ACL; same as last1.id). |
| Option B fallback | If T3-2 Edge Runtime fails on Deno deps: port functions to Express routes + systemd (estimate +3–5 days). Do not trigger unless Option A proof fails. |
Docker Compose recipe¶
Yes — working official recipe exists:
curl -fsSL https://supabase.link/setup.sh | sh(or sparse-clonesupabase/docker)- Configure
SUPABASE_PUBLIC_URL,API_EXTERNAL_URL,SITE_URLfor internal LAN + later prod sh run.sh start- Deploy functions: sync
RealOutcomes/supabase/functions/→volumes/functions/<name>/index.ts - Docs: Self-Hosting with Docker, Self-Hosted Functions, Self-Hosted S3
2. Auth unification feasibility (auth.last1.id)¶
Current state¶
| Item | Detail |
|---|---|
| Auth | Native Supabase Auth (GoTrue on product project) |
| Users | 3 (ryan@last1.org, rydaunicorn@gmail.com, ryan@last1.enterprises) |
| Client | supabase.auth.signInWithPassword, OTP, Google OAuth (client/src/pages/login.tsx) |
| Server | supabaseAdmin.auth.getUser(token) (server/auth.ts) |
| RLS | Service-role only; no auth.uid() dependency |
| User data | public.users FK = auth.users.id; 1 profile, 1 watchlist, 0 Stripe subs |
Unification scope (if pursued)¶
| Work item | Effort |
|---|---|
Register RealOutcomes OIDC client on auth.last1.id |
0.5 d |
| Replace client login with OIDC redirect + callback | 1 d |
Replace server token validation (JWKS from auth.last1.id) |
0.5–1 d |
| Remap or re-create 3 users + saved data FKs | 0.5 d |
| Google OAuth dedup / redirect URI matrix | 0.5 d |
| Parallel soak + admin gateway regression | 0.5–1 d |
| Total | ~2–4 days (not bounded ~1 day) |
Recommendation: DEFER (separate "identity unification" workstream)¶
Rationale:
- Tier 3 goal is compute + DB sovereignty, not portfolio SSO.
- Self-hosted GoTrue migration is straightforward (include
authschema in dump; 3 users can re-login or reset password). - Unification adds cross-product coupling during the first full-stack Supabase Docker migration (already the critical path).
- RLS does not block deferral (service-role architecture).
If deferred: Tier 3 ships R940 GoTrue at http://192.168.1.98:8000/auth/v1 with new JWT keys at cutover (closes GG 29169497 naturally).
3. VM planning¶
Recommendation: separate compute + platform VMs (do not consolidate)¶
Matches last1.id pattern (VM 113 db + VM 114 app). Supabase stack is memory-heavy; IRS import + Docker build on app VM should not compete with Edge Runtime + Postgres.
| VM | VMID | Hostname | LAN IP | Spec | Role |
|---|---|---|---|---|---|
| App | 116 | realoutcomes-r940 |
192.168.1.99/24 | 2 vCPU, 4 GB RAM, 50 GB boot | Express Docker :5000, monthly-irs-sync systemd timer, node_exporter |
| Supabase platform | 117 | realoutcomes-supabase |
192.168.1.98/24 | 4 vCPU, 16 GB RAM, 50 GB boot + 150 GB ZFS data | Supabase Docker Compose, Postgres 17, Edge Functions, Storage, GoTrue, Kong :8000, pg_cron, function cron timers, postgres_exporter |
IPs: .98/.99 next after admin .97 and auth-last1id .96. Confirm no collision on pve-r940 before provision (T3-2).
Tailscale: Join both VMs to tailnet; BWS DATABASE_URL uses LAN per portfolio convention.
Ingress: lastvet-edge tunnel → VM 116 :5000 at cutover (same pattern as n8n/admin).
4. Railway env audit¶
Source: Railway GraphQL read-only (LAST1_PLATFORM_RAILWAY_PROD_API_TOKEN).
Project: disciplined-beauty (0cb8d8fa-5216-479a-b1f5-00d2dfa7c88b)
Environment: production (12ba2dc7-8e11-4ab4-a4d9-9ecfecb47533)
| Service | ID | Domain |
|---|---|---|
| RealOutcomes.app | b048dff1-e5d7-446f-8eb4-87d57950aca1 |
realoutcomes.app |
| monthly-irs-sync | 938c60c8-68e5-45e9-bac0-e49b07d03ad9 |
(none) |
Web service env keys (24, excl. RAILWAY_*)¶
ADMIN_GATEWAY_SECRET, DATABASE_URL, FEC_API_KEY, FIRECRAWL_API_KEY, LAST1_CERT_REGISTRY_URL, NODE_ENV, OPENAI_API_KEY, PUBLIC_URL, SENDGRID_API_KEY, SENDGRID_FROM_EMAIL, SESSION_SECRET, STRIPE_FUNDER_MONTHLY_PRICE_ID, STRIPE_FUNDER_YEARLY_PRICE_ID, STRIPE_INVESTIGATOR_MONTHLY_PRICE_ID, STRIPE_INVESTIGATOR_YEARLY_PRICE_ID, STRIPE_PRO_MONTHLY_PRICE_ID, STRIPE_PRO_YEARLY_PRICE_ID, STRIPE_PUBLISHABLE_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_URL, VITE_SUPABASE_ANON_KEY, VITE_SUPABASE_URL
Cron service env keys (2)¶
DATABASE_URL, NODE_ENV (inherits DB creds; full set not duplicated on Railway)
Constraint honored: No Railway changes in T3-1.
Sizing note: Railway API did not return memory limits. App VM 2 vCPU / 4 GB matches admin + n8n precedent. Supabase VM sized up for Postgres + 8 Edge Functions.
5. BWS project¶
| Option | Verdict |
|---|---|
| New project | Not needed |
Existing realoutcomes (788ed6ec-0284-4bbc-b57c-b4840004688e) |
Recommended |
Current keys (3): realoutcomes_url, realoutcomes_web, realoutcomes_server (marketing/deploy metadata).
Add alongside (do not delete legacy until T3-7): REALOUTCOMES_R940_PROD_* per Tier 2 naming pattern (LAST1_ID_R940_PROD_* precedent in Last1.id project).
D-live rotation window (approved)¶
| GG ID | Secret | T3 action |
|---|---|---|
| 29169495 | Stripe webhook | Rotate in Stripe Dashboard → BWS before T3-5 |
| 29169499 | SendGrid | Rotate → BWS |
| 29169500 | Firecrawl | Rotate → BWS |
| 29169498 | Postgres | Replaced by R940 creds at stack init |
| 29169496 | OpenAI | Invalid in GG; reissue if production usage confirmed |
| 29169497 | Supabase JWT | New keys from R940 stack at cutover |
Do not rotate ADMIN_GATEWAY_SECRET independently (shared with last1-admin until Tier 6).
T3-2 action: Run Railway→BWS sync script (to be added); Ryan rotates vendor keys; store rotated values in REALOUTCOMES_R940_PROD_*.
6. Ryan decisions captured (2026-08-28)¶
| # | Decision | T3-1 disposition |
|---|---|---|
| 1 | Option A self-hosted Supabase | Accepted; gaps listed above |
| 2 | Marketing split deferred | Reflected in plan + muster |
| 3 | D-live rotation during BWS | Accepted; mapped to GG IDs |
| 4 | Auth unification | Defer; recommendation documented |
7. STOP — Next gate¶
| Phase | Status | Ryan action |
|---|---|---|
| T3-0 Audit | Done | Approved |
| T3-1 Planning | Done | Approve T3-2 kickoff |
| T3-2 DB + Supabase stack | Blocked | Authorize VM 116/117 provision + Gate B |
| T3-3 App parallel soak | Blocked | — |
| T3-4 Edge/cron/Stripe | Blocked | — |
| T3-5 DNS cutover | Blocked | — |
| T3-6 Soak | Blocked | — |
| T3-7 Teardown | Blocked | — |
T3-2 will: provision VMs 116/117, deploy Supabase Docker, Gate B dump/restore rehearsal, populate BWS REALOUTCOMES_R940_*, without DNS cutover or hosted teardown.
8. Related documents¶
- REALOUTCOMES_MIGRATION_PLAN.md
- TIER_RESEQUENCING_2026-08-28.md
- TIER_3_ADMIN_PAUSED_STATUS.md
- Muster:
migrate-realoutcomes-railway-supabase-to-r940