Last 1 Enterprises — Infrastructure Consolidation Plan¶
Status: Tier 1 (n8n) executed (2026-08-25). Tier 2 (last1.id Mode 2A) CLOSED (2026-08-26). Tiers 3–6 renumbered (see below).
Branch: Tier 1 merged to main. Tier 2 plan: LAST1_ID_MIGRATION_PLAN.md (PR #35 → merge after doc sync).
Scope: Migrate Last 1 products off Railway + Supabase onto R940, one product (or tightly coupled bundle) at a time. SendGrid → RealSend is a separate track (pattern proven on MATR).
Drivers: cost, sovereignty story consistency, SOC 2 path for Onset, security posture.
Method: Tier 1 proved the generic playbook on n8n; higher tiers follow audit → provision → cutover → soak (tier-appropriate) → teardown.
Related: R940_PLATFORM_ARCHITECTURE.md · LASTVET_MIGRATION_PLAN.md · BITWARDEN_NAMING_CONVENTION.md · Muster decide-r940-consolidation-scope-for-remaining-la / reference-matr-sendgrid-realsend-migration-patte
Executive summary¶
R940 has headroom (12 VMs, ~444 GiB RAM allocated of ~3 TiB; tank-fast ~13.8T free; tank-bulk ~55T free). Capacity is not the blocker. Coupling is.
| Finding | Implication |
|---|---|
Shared Supabase tcthzakppndkykatoefx (“Last 1 App + Radio Check”) is used by last1.app, RadioCheck, and last1-admin auth |
Tier 6 must move as a bundle (or split the DB first). last1-admin compute can move earlier only if it keeps pointing at this project until the bundle cutover. |
| monthly-irs-sync is not a product | It is a Railway cron inside RealOutcomes (railway.cron.toml). Fold into RealOutcomes migration; do not schedule as a separate Tier 1 “product.” |
| onset-web is already off Railway | Cloudflare Pages. Railway/Supabase work for Onset is primarily clinic.theonset.app (+ shared Onset Supabase for mobile). |
| last1.id is universal SSO IdP | RadioCheck, LastVet, Onset, last1-admin, and future products depend on last1.id OAuth/trust events. Tier 2 = last1.id (moved up from Tier 4). Downstream tiers assume sovereign identity on R940 first. |
| Per-tenant Postgres remains the R940 rule | Do not dump all products into one shared Postgres. Keep clinical/identity boundaries. The existing last1.app↔RadioCheck share is a legacy exception to resolve deliberately in Tier 6. |
| Live Railway memory/replicas and Supabase sizes | Not readable from this host (no Railway CLI session; Supabase MCP unauthenticated; BWS not queried for DB passwords). Repo-committed config + project refs are below; dashboards needed for exact $ and row counts. |
| Recommended BW pattern | Per-product prefixes (LAST1ID_R940_*, ONSET_R940_*, …), not a universal LAST1_R940_* namespace. Aligns with existing LASTVET_ / ANGEL_ / SOVEREIGN_ / MERIDIAN_. Extension drafted in naming convention doc. |
Recommended tier order (Ryan, 2026-08-24):
| Tier | Product | Rationale |
|---|---|---|
| 1 | n8n | Warm-up ✅ done |
| 2 | last1.id (Mode 2A sovereign stack) | Universal SSO; downstream products need identity first |
| 3 | last1-admin compute | Internal; keeps shared Supabase until Tier 6 |
| 4 | RealOutcomes (+ IRS cron) | Bounded app; own Supabase; includes marketing/app split |
| 5 | Onset clinic + Supabase | SOC 2 driver |
| 6 | last1.app + RadioCheck + angel.report app tier | Shared auth DB bundle; includes last1.app marketing/app split |
Parallel: SendGrid → RealSend (not interleaved with DB cutovers).
Parallel (complete): Cloudflare Pages marketing track (M1 audit 2026-08-24). No separate marketing migration tier. See Marketing sites.
STOP (Week 1 execution): Ryan approves kickoff date in LAST1_ID_MIGRATION_PLAN.md before VM provisioning.
Marketing sites (Cloudflare Pages track: complete)¶
Status: M1 audit accepted (2026-08-24). Separate marketing migration track closed. No DNS or Pages project changes from this track.
| Site | Pages project | Notes |
|---|---|---|
last1.enterprises / www |
last1ent-web |
Live. Legacy *.pages.dev host is still radiocheck-web-8cz.pages.dev (immutable without new project + DNS; cosmetic polish deferred) |
last1.capital / www |
last1capital-web |
Live |
last1.studio / www |
last1studio-web |
Live |
last1.org / www |
last1org-web |
Live |
veteransco.org / www |
veteransco-web |
Live |
angel.report / www (marketing) |
angel-web |
Live. App API stays api.angel.report (Workers). Login → Tier 6 / auth.last1.id, not this track |
Not in this track (deferred into product tiers):
| Surface | Hosting today | Bundled into |
|---|---|---|
last1.app apex (marketing + app monolith) |
Railway | Tier 6 (marketing/app split + auth.last1.id) |
realoutcomes.app apex (marketing + app monolith) |
Railway | Tier 4 (marketing/app split on R940 move) |
admin.last1.enterprises |
Railway | Tier 3 (last1-admin compute) |
last1.id marketing |
Cloudflare Pages (Tier 2 plan) | Tier 2 (not this track) |
Onset marketing (theonset.app) |
Cloudflare Pages (onset-web) |
Tier 5 clinic only; marketing already Pages |
Principle: when Tier 4 / Tier 6 execute, include marketing/app split (static marketing → Cloudflare Pages; app tier → R940 / existing app host). Not a separate marketing migration.
Muster: cloudflare-pages-marketing-migrations → done (audit reframed: already hosted).
Tier 1 complete — n8n (2026-08-25)¶
| Item | Outcome |
|---|---|
| Production URL | https://n8n.last1.enterprises/ (R940 VM 112, 192.168.1.94) |
| Ingress | Cloudflare tunnel on lastvet-edge → LAN n8n |
| BWS pattern | N8N_R940_PROD_* in last1-platform |
| Archive | backups/n8n-railway-teardown-2026-08-25/ (4 workflow JSON + metadata) |
| Legacy DNS | n8n.theonset.app deleted (no CNAME fallback) |
| Soak | Skipped (Ryan founder call; non-critical marketing automation) |
| Observability | Prometheus + Loki + Grafana dashboard n8n R940 |
| Railway service | Deleted 2026-08-24 (n8n service 8fbf94ec-b660-4950-935a-e848366ea727); verify ~$5–20/mo savings next billing cycle |
Runbook: N8N_MIGRATION_RUNBOOK.md
Tier 1 lessons learned (not universal)¶
- API workflow audit found 4 workflows (Ryan manual count: 3); inactive duplicate excluded from R940 import.
- n8n public API needs browser User-Agent through Cloudflare.
- Fresh encryption key → credential re-entry + Google Drive/Sheets APIs on OAuth GCP project.
cloudflaredonlastvet-edge+ CNAME to{tunnel-id}.cfargotunnel.comworks for*.last1.enterprisesservices.- Soak skip acceptable Tier 1 only; Tier 2 last1.id uses 24–48h (Ryan founder call, 0 users); Tiers 3–6 keep tier-appropriate soak (table below).
- Store
LAST1_PLATFORM_RAILWAY_PROD_API_TOKENin BWS before Tier 2 for teardown automation (done 2026-08-24 inlast1-platform). - Export hygiene: workflow/config exports may contain plaintext secrets (e.g. inline bearer tokens in n8n JSON). Tier 1 archive was committed before gitignore; Ryan accepted exposure because credentials deprecate with Railway/Onset migration. Tier 2+ (last1.id, RealOutcomes, Onset): do not commit export archives unencrypted; gitignore from creation or store encrypted off-repo. Git history is not purged for Tier 1 (internal repo, accepted risk).
Tier 2 lessons learned — last1.id database migration (Gate B, 2026-08-25)¶
Applies to Tier 3–6 Supabase → R940 Postgres migrations:
- IPv4 from R940: Hosted Supabase direct host
db.[ref].supabase.cois IPv6-only. R940 VMs are IPv4-only today. Use Supavisor session pooler (aws-*-[region].pooler.supabase.com:5432, userpostgres.[ref]) forpg_dump/ read-only audits. Discover region by probe or dashboard Connect panel (last1.id:aws-1-us-east-1). pg_dumpclient version: Dump client major version must be ≥ source Postgres major (hosted 17.x; auth VM PG 16 client failed). Run dumps from sovereign-db VM (PG 17) or install matchingpostgresql-client-17.- Post-restore auth ownership: After
pg_restore,auth.*tables must be owned by the GoTrue DB role (last1id_appon Tier 2). WithoutALTER … OWNER TO, GoTrue fails startup (must be owner of table refresh_tokens). Script:scripts/last1-id/gate-b-restore.sh. - GoTrue version vs Supabase auth migrations: Self-hosted GoTrue must match the Supabase
auth.schema_migrationsstate you restored. Check latest migration timestamp against GoTrue release notes before Gate C. Tier 2 Gate C required v2.170.0 → v2.196.0 after refresh failed withmissing destination name oauth_client_id. Future Tier N Supabase → self-hosted GoTrue migrations: verify compatibility before Gate C smoke tests, not after. - BWS project routing (Week 4):
LAST1_ID_R940_*secrets belong in theLast1.idBWS project (cffb320a…), notlastvet. Week 3 scripts defaulted to wrong project ID; corrected viamigrate-last1id-bws-to-project.sh. - OAuth credential intake (Week 4): When Ryan's new GCP client is in an auth-gated Doc, agent must STOP and ask — never infer from
last1-admingenericGOOGLE_OAUTH_*keys. Wrong client causedredirect_uri_mismatchuntil Ryan updated BWS directly. - Week 5 failure-mode testing: Host
iptablesdoes not block Dockerized GoTrue → Postgres on another VM. Use Docker pause, DB VM stop, or bridge network policy for meaningful DB-down probes. - Partner OIDC
subclaim:id_token.submay be partner-scoped UUID distinct fromauth.users.id; validate against LastVet contract, not raw GoTrue user id.
Export archive policy (Tiers 2–6)¶
| Tier | Risk | Archive handling |
|---|---|---|
| 1 (n8n) | Marketing automation; secrets obsolete post-teardown | Committed once; backups/**/*.json gitignored going forward |
| 2 (last1.id) | Identity authority; gitignore DB/auth exports | Encrypted off-repo or gitignored from day one |
| 3 (last1-admin) | Internal-only; moderate | Gitignore JSON exports at capture |
| 4+ (RealOutcomes, Onset, bundle) | Customer / identity / clinical data | Encrypted off-repo or gitignored from day one; never plaintext in git |
Recommended soak by tier (Tiers 2–6)¶
| Tier | Product | Recommended soak |
|---|---|---|
| 2 | last1.id (Mode 2A) | 24–48h (Ryan founder call; 0 production users; Railway + Supabase kept as fallback) |
| 3 | last1-admin compute | 24–48h (internal-only) |
| 4 | RealOutcomes (+ IRS cron) | 48–72h (customer data) |
| 5 | Onset clinic + Supabase | 7 days minimum (clinical / SOC 2 driver) |
| 6 | last1.app + RadioCheck + angel.report bundle | 7 days minimum (shared auth DB complexity) |
Tier 2 — last1.id Mode 2A — CLOSED 2026-08-26¶
Ryan decisions locked (2026-08-24): Mode 2A sovereign stack · Tier 2 numbering · split hostname · dedicated GCP OAuth · 24–48h soak.
Outcome: Production identity at https://auth.last1.id on R940. Railway last1.id service, hosted Supabase nztgswcpnwggymncrnyx, and legacy GCP OAuth client deleted after W7 archival. BWS Last1.id project: 33 live keys.
Reports: Gate A/B/C · W1–W6 · W7-1 through W7-6 · LAST1_ID_MIGRATION_PLAN.md (Tier 2 closeout summary) · LAST1_ID_MIGRATION_RUNBOOK.md (ops).
| Item | Target state | Status |
|---|---|---|
| Compute | auth-last1id VM (GoTrue + Kong + last1.id app) |
Live |
| Database | Postgres 17 on sovereign-db-last1id |
Live |
| Primary auth URL | https://auth.last1.id |
Live |
| Marketing URL | https://last1.id → Cloudflare Pages |
Live |
| GCP OAuth | Dedicated project client 889079898378-… |
Live |
| Hosted Supabase / Railway | Deleted; archive in backups/last1id-railway-teardown-2026-08-26/ |
Gone |
Deferred: iOS issuer https://last1.id; my.last.vet Last1 sign-in; provider staging issuer cleanup.
Rationale for Tier 2 before admin/RealOutcomes: last1.id is the planned universal SSO IdP. Tier 2 complete — proceed to Tier 3 when ready.
Investigation limits (honesty box)¶
| Source | Status |
|---|---|
| GitHub repos (Last-1-Enterprises) | Read via gh API |
Proxmox pve-r940 (qm list, zpool, node memory) |
Read via SSH |
| Railway dashboard (memory, replicas, live env, $) | Unavailable (railway CLI not installed/authed) |
| Supabase dashboard (disk size, extensions UI, live RLS count, MAU) | Unavailable (MCP needsAuth; no project passwords used) |
| BWS secret values | Not touched (constraint). Key names in last1-admin project were previously known from ops context; not re-listed here |
| User counts / PHI volume | Not determinable without DB login |
Where numbers are approximate, they are labeled.
Sub-task A — Product inventory¶
Shared map (dependencies)¶
last1.id ←── RadioCheck (edge last1-id-callback, OAuth)
←── LastVet / Onset (LAST1_ID_OAUTH_* / Connect)
←── last1-admin (LAST1_ID_API_BASE + gateway)
Supabase tcthzakppndkykatoefx ("Last 1 App + Radio Check")
├── last1.app (Railway app)
├── RadioCheck (mobile + edge functions)
└── last1-admin (auth / admin profiles)
Onset Supabase (project ref not in clinic .temp; separate from above)
├── clinic.theonset.app (Railway)
├── onset-ios / onset-android
└── last1-admin (ONSET_SUPABASE_* for admin dashboards)
RealOutcomes Supabase cpocwrygxlqhzvmlrpjv
├── RealOutcomes (Railway)
└── monthly-irs-sync (Railway cron in same repo)
onset-web → Cloudflare Pages only (no Railway)
radiocheck-web → separate Supabase yloemokdrxfjkfwkskim (marketing/Stripe functions)
n8n-railway → Railway only (no Supabase in repo)
1. last1.id¶
| Field | Finding |
|---|---|
| Repo | https://github.com/Last-1-Enterprises/last1.id · main · last push 2026-08-10 · TypeScript |
| Railway | railway.toml: Dockerfile builder; startCommand = node dist/index.cjs; healthcheck /api/health; restart on_failure ×10. Dockerfile: Node 22 alpine (WebSocket requirement). Port 5000. Memory/replicas: unknown (dashboard). |
Env (from .env.example / README) |
SUPABASE_*, VITE_SUPABASE_*, CREDENTIAL_SIGNING_KEY, optional Turnstile/Twilio, ADMIN_GATEWAY_SECRET, optional SENDGRID_API_KEY (org-admin handoff). Build args: VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY. |
| Supabase | Project nztgswcpnwggymncrnyx · Postgres 17.6.x (.temp/postgres-version) · Storage + Auth enabled · 71 SQL migrations · sampled ~34 CREATE POLICY / ~34 ENABLE RLS in first 50 files (underestimate if later files denser) · extensions: not fully enumerated without dashboard |
| Effort | L (identity authority, DID/VC, many dependents) |
| Dependencies | Upstream for RadioCheck, LastVet/Onset OAuth, last1-admin |
| Users | Unknown |
| Data posture | Identity, credentials, consent, trust events. Not clinical PHI, but high-sensitivity PII + signing keys |
| SendGrid | Optional; from-address example integrations@last1.id in deploy notes; transactional handoff |
2. onset (+ theonset / clinic)¶
| Surface | Hosting | Notes |
|---|---|---|
| clinic.theonset.app | Railway + Supabase | Clinic portal (PHI-adjacent clinical workflows, Fullscript) |
| onset-web | Cloudflare Pages | Marketing theonset.app / www.theonset.app — out of Railway migration |
| onset-ios / onset-android | App stores + Supabase | Clients of Onset Supabase |
| Field | clinic.theonset.app |
|---|---|
| Repo | https://github.com/Last-1-Enterprises/clinic.theonset.app · main · push 2026-08-10 |
| Railway | Checklist-driven Next app; NEXT_PUBLIC_APP_URL=https://clinic.theonset.app. No railway.toml in tree snapshot used for this report (deploy via Railway + checklist). Memory/replicas unknown. |
| Env | Supabase URL/keys, Fullscript, Stripe, SENDGRID_API_KEY, NOTIFY_FROM_EMAIL=Onset Clinic <notifications@theonset.app>, support inbox ryan@theonset.app, admin deep link to admin.last1.enterprises |
| Supabase | 42 migrations · ~48 CREATE POLICY · ~14 ENABLE RLS (sampled all 42) · Edge functions (fitness/protocol extract, practitioner-notifications) · Project ref not committed in .temp |
| Effort | XL (clinical + SOC 2 narrative + Fullscript + edge functions + mobile clients) |
| Dependencies | last1-admin Onset dashboards; last1.id Connect (platform assertion secret); Fullscript |
| Users | Unknown |
| Data posture | Clinical / PHI-adjacent (patients, protocols, practitioner tools). SOC 2 driver. |
| SendGrid | notifications@theonset.app transactional; marketing updates@theonset.app via last1-admin |
3. last1.app¶
| Field | Finding |
|---|---|
| Repo | https://github.com/Last-1-Enterprises/last1.app · main · push 2026-08-10 |
| Railway | Dockerfile Node 20; Vite build args for Supabase/Stripe/admin flags; CMD node dist/index.cjs port 5000. Separate admin.last1.app build flags (VITE_ADMIN_ONLY, PUBLIC_URL) documented in railway.toml. Memory/replicas unknown. Apex DNS still Railway (k1v3g8rr.up.railway.app / www sibling). |
| Marketing/app | Monolith today. Marketing extraction → Cloudflare Pages is Tier 6 scope (not a standalone marketing migration). Login today is in-app (/api/auth/last1/login + Supabase); auth.last1.id handoff rides Tier 6 / last1.id. |
| Supabase | Same project as RadioCheck/admin: tcthzakppndkykatoefx (“Last 1 App + Radio Check”) · last1.app repo has only 1 checked-in migration (schema mostly lives with RadioCheck history) |
| Effort | L alone; XL as bundle with RadioCheck (includes marketing/app split) |
| Dependencies | Shared DB with RadioCheck; Stripe; SendGrid; last1-admin gateway; IRS validation code paths distinct from RealOutcomes cron |
| Users | Unknown |
| Data posture | Org/funder platform data; beneficiary PII encryption mentioned in env comments; not LastVet PHI |
| SendGrid | noreply@last1.app (from last1.app .env.example / RadioCheck shared notes) · transactional |
4. radiocheck¶
| Field | Finding |
|---|---|
| Repos | https://github.com/Last-1-Enterprises/RadioCheck (mobile) · https://github.com/Last-1-Enterprises/radiocheck-web (web) |
| Railway | Mobile: none (App Store). Backend is Supabase (Auth/Realtime/Edge). radiocheck-web: Supabase functions (Stripe, form notify); not a classic Railway Node service in inventory |
| Supabase (app) | tcthzakppndkykatoefx · Postgres 17.6.x · 87 migrations · sampled 50 files: ~149 CREATE POLICY / ~63 ENABLE RLS (full count likely higher) · many Edge Functions (check-in, SMS, last1-id-callback, delete-account, …) |
| Supabase (web) | Separate project yloemokdrxfjkfwkskim |
| Effort | XL (edge functions + Realtime + shared DB + mobile cutover) |
| Dependencies | last1.id; shared DB with last1.app; Twilio; SendGrid on Pro/Enterprise |
| Users | Unknown |
| Data posture | Veteran check-in / org SMS; sensitive but not LastVet clinical EHR. Treat carefully. |
| SendGrid | Shared noreply@last1.app pattern for Pro/Enterprise email |
5. realoutcomes (+ monthly-irs-sync)¶
| Field | Finding |
|---|---|
| Repo | https://github.com/Last-1-Enterprises/RealOutcomes · main · push 2026-08-10 |
| Railway app | railway.toml: DOCKERFILE; restart ON_FAILURE ×3. Start via Dockerfile (standard Node dist). Apex DNS Railway (yi6abdjh.up.railway.app / www sibling) on zone realoutcomes.app. |
| Marketing/app | Monolith today. Marketing extraction → Cloudflare Pages is Tier 4 scope (not a standalone marketing migration). |
| Railway cron | railway.cron.toml: startCommand = node dist/cron/monthly-irs.cjs; schedule 0 3 1 * *; restart NEVER. This is monthly-irs-sync. |
| Supabase | cpocwrygxlqhzvmlrpjv · Postgres 17 · 17 migrations · ~16 policies / ~19 RLS enables · Edge functions (crawl, score, alerts, validations) · Storage bucket uploads |
| Effort | M–L (Stripe tiers, OpenAI/Firecrawl, edge functions, IRS cron, marketing/app split) |
| Dependencies | last1-admin (ADMIN_GATEWAY_SECRET); optional last1 cert registry |
| Users | Unknown (Stripe plans imply some paying users; count not read) |
| Data posture | Nonprofit outcomes / grant data; not clinical PHI |
| SendGrid | hello@realoutcomes.app transactional |
6. last1-admin¶
| Field | Finding |
|---|---|
| Repo | https://github.com/Last-1-Enterprises/last1-admin · main · push 2026-08-18 |
| Railway | Dockerfile + Vite build args (VITE_SUPABASE_*, VITE_LAST1_API_BASE). Domain admin.last1.enterprises (Tier 3 compute scope; not marketing Pages). |
| Supabase | Linked to tcthzakppndkykatoefx (same as last1.app + RadioCheck). Also talks to Onset and RadioCheck service-role URLs and LastVet/Angel APIs (not local DBs in prod). |
| Effort | M for compute move; L if auth DB moves with Tier 6 bundle |
| Dependencies | Proxies: last1.app, last1.id, realoutcomes, onset, last.vet, angel.report |
| Users | Internal operators only |
| Data posture | Admin metadata; holds service-role keys to multiple products. High blast radius if compromised. |
| SendGrid | Marketing senders for Onset (updates@theonset.app) and LastVet marketing tables |
7. n8n¶
| Field | Finding |
|---|---|
| Repo | https://github.com/Last-1-Enterprises/n8n-railway · main · push 2026-04-17 |
| Railway | FROM n8nio/n8n + start.sh (/data/.n8n). No Supabase. Minimal surface. |
| Effort | S (ideal Tier 1 warm-up) |
| Dependencies | Whatever workflows point at (needs live n8n audit at migrate time) |
| Users | Internal automation |
| Data posture | Workflow credentials may include secrets; inventory workflows before move |
| SendGrid | None in repo (workflows may call SendGrid; unknown) |
8. monthly-irs-sync¶
Not a standalone product. See RealOutcomes cron above. Migration effort folds into RealOutcomes (add a systemd timer or cron on the RealOutcomes R940 VM).
Sub-task B — R940 capacity¶
Current inventory (2026-08-20, qm list on pve-r940)¶
| VMID | Name | RAM (MiB) | Boot disk (GB) |
|---|---|---|---|
| 100 | agent-01 | 98304 | 200 |
| 101 | inference-01 | 65536 | 150 |
| 102 | meridian-eval | 32768 | 100 |
| 103 | meridian-rag | 65536 | 100 |
| 104 | sovereign-app | 32768 | 100 |
| 105 | sovereign-mem | 32768 | 100 |
| 106 | angel-local | 49152 | 100 |
| 107 | lastvet-api | 16384 | 100 |
| 108 | lastvet-db | 32768 | 100 |
| 109 | lastvet-imaging | 8192 | 100 |
| 110 | lastvet-edge | 4096 | 50 |
| 111 | observability | 16384 | 50 |
- VM count: 12 running
- RAM allocated: ~444 GiB of ~3.0 TiB host (~14.5%). ~2.8 TiB available on host metrics.
- Storage:
tank-fast13.9T ~13.8T free;tank-bulk55.9T ~55.3T free (ZFS avail metrics).
Per-product VM vs share¶
| Product | Recommendation |
|---|---|
| n8n | Small shared ops VM or dedicated 2 vCPU / 4–8 GB |
| last1-admin | Dedicated small app VM (or share ops VM initially) |
| RealOutcomes | App VM + own Postgres (or Postgres on same VM if tiny; prefer separate DB disk) |
| last1.id | Dedicated app + dedicated Postgres (identity blast radius) |
| Onset clinic | Dedicated app + dedicated Postgres (clinical boundary) |
| last1.app + RadioCheck | One shared Postgres until intentional split; 1–2 app tiers (API/edge replacement) |
| Object storage | Prefer Supabase Storage → MinIO only where needed; do not put clinical objects on lastvet-imaging without a new trust-boundary design |
Postgres consolidation decision¶
Decision recommendation (aligns with R940 architecture):
- Default: one Postgres per trust boundary (LastVet already separate; Onset separate; last1.id separate; RealOutcomes separate).
- Exception: keep last1.app + RadioCheck on one Postgres through Tier 6 cutover; schedule a later split only if product/legal boundaries require it.
- Reject: single “all Last1” Postgres on one VM. Violates documented per-tenant sovereignty and complicates SOC 2 / incident scope.
MinIO¶
LastVet already owns lastvet-imaging. New product object stores should be new ZFS datasets / MinIO instances (or temporary continue Supabase Storage until product cutover). No evidence these Railway products currently use R940 MinIO.
Sub-task C — Sequencing recommendation¶
Ryan’s tier order (locked 2026-08-24)¶
| Tier | Product |
|---|---|
| 0 | Prerequisites (playbook, BW naming, capacity) |
| 1 | n8n warm-up ✅ |
| 2 | last1.id (Mode 2A sovereign identity) |
| 3 | last1-admin compute |
| 4 | RealOutcomes (+ IRS cron) |
| 5 | Onset clinic + Supabase |
| 6 | last1.app + RadioCheck + angel.report app tier |
Adjustments¶
| Change | Why |
|---|---|
| Tier 2 = last1.id (Mode 2A) | Ryan founding intent: full sovereign identity (Postgres + GoTrue + MinIO on R940). Universal SSO means downstream products need this first. |
| Tier 3 = last1-admin compute | Move Railway Node to R940; keep Supabase auth on tcthzakppndkykatoefx until Tier 6. |
| Tier 4 = RealOutcomes including IRS cron | Bounded product; own Supabase; rehearsal after identity. Includes marketing/app split: apex today is a Railway monolith; extract static marketing → Cloudflare Pages as part of this tier (not a separate marketing track). |
| Tier 5 = Onset clinic + Onset Supabase | SOC 2 driver. onset-web stays on Cloudflare. |
| Tier 6 = last1.app + RadioCheck + angel.report (+ last1-admin auth DB) | Mandatory bundle. Highest coordination cost. Includes marketing/app split for last1.app (and angel.report login → auth.last1.id per marketing/app principle). Marketing hosting for angel.report apex is already Pages. |
| Parallel: SendGrid → RealSend | Do not interleave domain warmup with DB cutovers. |
Suggested calendar shape (planning only)¶
- Approve Tier 2 execution plan + Week 1 kickoff date.
- last1.id Mode 2A (5–8 weeks).
- last1-admin compute (2–4 days).
- RealOutcomes full (1–2 weeks).
- Onset clinic (multi-week; counsel/SOC 2 aware).
- last1.app + RadioCheck + angel.report bundle (multi-week).
Effort estimates assume Cursor + Ryan review; LastVet AWS retirement lessons (~days per bounded workload) scale up for identity/clinical.
Sub-task D — Bitwarden naming decision¶
Recommendation: per-product prefix, not universal LAST1_R940_*.
| Option | Pros | Cons |
|---|---|---|
Universal LAST1_R940_* |
Fewer prefixes | Collides with LAST1_PLATFORM_*; weak blast-radius grep; fights existing LastVet/Angel pattern |
Per-product LAST1ID_R940_*, ONSET_R940_*, LAST1APP_R940_*, RADIOCHECK_R940_*, REALOUTCOMES_R940_*, LAST1ADMIN_R940_*, N8N_R940_* |
Matches LASTVET_ / ANGEL_; clear ownership; project-per-product in BWS |
Slightly more prefixes |
Rule: LAST1 / LAST1_PLATFORM remains cross-product only. Product runtime secrets use product codes above. Extension recorded in BITWARDEN_NAMING_CONVENTION.md.
Sub-task E — Generic migration playbook¶
Reuse per product. Pattern distilled from LastVet AWS→R940 retirement and R940 platform rules.
1. Pre-migration audit¶
- [ ] Confirm
mainSHA and open PRs - [ ] Export Railway service settings (build/start, memory, replicas, volumes, cron)
- [ ] Export Supabase: Postgres version, disk, extensions, RLS policy count, Auth providers, Storage buckets, Edge function list
- [ ] Data volume + user/org counts
- [ ] Dependency map (OAuth, webhooks, admin gateways)
- [ ] User impact window + rollback owner
2. R940 target provisioning¶
- [ ] Allocate VMID, hostname, LAN IP, MagicDNS (
*.tailc3bbdc.ts.net) - [ ] Size vCPU/RAM/disk from audit; place data on
tank-fast(or bulk if object-heavy) - [ ] Join Tailscale with tagged auth key pattern (
tag:r940-vm) when ready - [ ] UFW: Tailscale/LAN only for admin ports; public only via approved edge
3. Database / object store / secrets¶
- [ ] Provision Postgres (per trust boundary)
- [ ] Roles: migrate-style
*_master/*_appsplit where applicable - [ ] Restore logical dump from Supabase; verify extensions (pgcrypto, etc.)
- [ ] Recreate Storage → MinIO only if required; else temporary dual-run
- [ ] Create BWS keys under product prefix; map to runtime names in systemd drop-in
4. Code / config changes¶
- [ ] Prefer env-only cut (connection strings, URLs). Avoid app renames of BWS keys.
- [ ] Replace Supabase Auth carefully (GoTrue vs first-party auth is a product decision; default = keep Supabase Auth temporarily via self-host or cut Auth in a dedicated phase). Flag: Auth migration may dwarf “move Postgres.”
- [ ] Update redirect URIs, webhooks, Stripe endpoints
5. Deployment runbook¶
- [ ] systemd units (app + optional side workers)
- [ ] Health checks (
/api/healthor product equivalent) - [ ] Log shipping to observability (Promtail) when ready
- [ ] Backup: vzdump + logical dump schedule
6. Cutover¶
- [ ] DNS / Cloudflare / Railway custom domain drain
- [ ] Freeze writes if dual-write not available
- [ ] Final dump + restore or continuous replication catch-up
- [ ] Flip env; verify auth login, critical writes, admin proxy
- [ ] Watch error budgets 24–72h
7. Rollback¶
- [ ] Keep Railway service stopped but not deleted for N days
- [ ] Keep Supabase project until rollback window closes
- [ ] DNS TTL lowered before cutover
- [ ] Document “flip back” owner and checklist
8. Cleanup¶
- [ ] Delete Railway service/project after window
- [ ] Delete Supabase project after window + legal retention check
- [ ] Confirm invoice lines gone
- [ ] Remove retired
_RAILWAY_/ old secrets after rotation
9. Documentation¶
- [ ] Update R940_PLATFORM_ARCHITECTURE.md tenant map
- [ ] BWS manifest / naming
- [ ] Product runbook under
docs/ - [ ] Muster item →
donewith note
Auth special note: Several products are “Supabase = Auth + DB + Edge.” Moving DB only while leaving Auth on Supabase is a valid interim (hybrid). Moving Auth requires either self-hosted GoTrue/Supabase stack or product rewrites. Decide per product in Tier 0 kickoff.
Sub-task F — Cost analysis¶
Live invoices not read. Use as planning priors; replace with dashboard exports before committing savings claims.
| Line | Estimate basis | Monthly (order of magnitude) |
|---|---|---|
| Supabase projects (≥4–5 Pro-class) | Public Pro list price ~$25/project | ~$100–200+ if all Pro; Team/spend caps higher |
| Railway services (last1.id, clinic, last1.app, admin, RealOutcomes, n8n, IRS cron) | Usage + plan | ~$50–300+ highly usage-dependent |
| SendGrid | Shared account / multiple senders | ~$15–100+ tier-dependent |
| R940 marginal | Hardware already owned; power/network sunk | ~$0 compute marginal; ops time is the cost |
| Annual savings (compute hosting) | If Railway+Supabase ≈ $200–500/mo | ~$2.4k–6k/yr before labor; verify with invoices |
Not included: engineer time, RealSend fees, dual-run periods, SOC 2 audit spend (Onset driver is compliance, not pure hosting savings).
Sub-task G — SendGrid → RealSend (separate track)¶
Known sender domains / addresses¶
From product repos (active senders) plus the broader SendGrid authenticated-domain list Ryan already inventoried:
| Domain / address | Likely class | In consolidation scope? |
|---|---|---|
notifications@theonset.app / updates@theonset.app / theonset.app |
Transactional + marketing | Yes (Onset) |
hello@realoutcomes.app / realoutcomes.app |
Transactional | Yes |
noreply@last1.app / last1.app |
Transactional | Yes |
integrations@last1.id |
Transactional optional | Yes |
radiocheck.app |
Transactional (Pro/Enterprise) | Yes (often via last1.app sender) |
last.vet |
Transactional | LastVet track (already R940 path) |
angel.report |
Transactional | Angel (Cloudflare; separate) |
last1.enterprises, last1.capital, last1.studio, veteransco.org, www.last1.org |
Mixed / claimed | Confirm active vs territory before warmup |
Plan¶
- Inventory one SendGrid account: authenticated domains, PTR/SPF/DKIM, templates, webhook events.
- Confirm RealSend capacity + MATR lessons (
reference-matr-sendgrid-realsend-migration-patte). - Warmup calendar per domain (do not cut cold).
- Implement provider abstraction or env swap per product (
EMAIL_PROVIDER=realsend). - Coordinated cutover weekend; keep SendGrid read-only 14 days.
- Do not couple to Postgres cutover night.
Sub-task H — Muster items¶
Filed as status: later (see muster tracker.yaml on main after PR/commit). IDs:
infra-consolidation-playbook-approveinfra-bw-naming-r940-products-approvemigrate-n8n-railway-to-r940cloudflare-pages-marketing-migrations(done 2026-08-24; audit: 6/8 already Pages)migrate-last1-admin-compute-to-r940migrate-realoutcomes-railway-supabase-to-r940migrate-last1-id-railway-supabase-to-r940migrate-onset-clinic-supabase-to-r940migrate-last1app-radiocheck-shared-supabase-to-r940batch-sendgrid-to-realsend-last1-domains
Open questions for Ryan¶
- Accept per-product BWS prefixes as written?
- Week 1 kickoff date for Tier 2 last1.id Mode 2A (see LAST1_ID_MIGRATION_PLAN.md).
- Provide Railway + Supabase billing exports to harden Sub-task F.
- Post-Tier 2: Onset Automation OAuth duplicate under
last1appGCP project (hygiene; defer until after cutover).
Appendix — Supabase project refs (committed)¶
| Ref | Name / use |
|---|---|
nztgswcpnwggymncrnyx |
last1.id |
cpocwrygxlqhzvmlrpjv |
RealOutcomes |
tcthzakppndkykatoefx |
Last 1 App + Radio Check (+ last1-admin auth) |
yloemokdrxfjkfwkskim |
radiocheck-web |
| (unknown in git) | Onset clinical |
End of planning document. No migration actions authorized by this file alone.