Skip to content

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)

  1. API workflow audit found 4 workflows (Ryan manual count: 3); inactive duplicate excluded from R940 import.
  2. n8n public API needs browser User-Agent through Cloudflare.
  3. Fresh encryption key → credential re-entry + Google Drive/Sheets APIs on OAuth GCP project.
  4. cloudflared on lastvet-edge + CNAME to {tunnel-id}.cfargotunnel.com works for *.last1.enterprises services.
  5. 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).
  6. Store LAST1_PLATFORM_RAILWAY_PROD_API_TOKEN in BWS before Tier 2 for teardown automation (done 2026-08-24 in last1-platform).
  7. 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:

  1. IPv4 from R940: Hosted Supabase direct host db.[ref].supabase.co is IPv6-only. R940 VMs are IPv4-only today. Use Supavisor session pooler (aws-*-[region].pooler.supabase.com:5432, user postgres.[ref]) for pg_dump / read-only audits. Discover region by probe or dashboard Connect panel (last1.id: aws-1-us-east-1).
  2. pg_dump client 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 matching postgresql-client-17.
  3. Post-restore auth ownership: After pg_restore, auth.* tables must be owned by the GoTrue DB role (last1id_app on Tier 2). Without ALTER … OWNER TO, GoTrue fails startup (must be owner of table refresh_tokens). Script: scripts/last1-id/gate-b-restore.sh.
  4. GoTrue version vs Supabase auth migrations: Self-hosted GoTrue must match the Supabase auth.schema_migrations state 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 with missing destination name oauth_client_id. Future Tier N Supabase → self-hosted GoTrue migrations: verify compatibility before Gate C smoke tests, not after.
  5. BWS project routing (Week 4): LAST1_ID_R940_* secrets belong in the Last1.id BWS project (cffb320a…), not lastvet. Week 3 scripts defaulted to wrong project ID; corrected via migrate-last1id-bws-to-project.sh.
  6. 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-admin generic GOOGLE_OAUTH_* keys. Wrong client caused redirect_uri_mismatch until Ryan updated BWS directly.
  7. Week 5 failure-mode testing: Host iptables does not block Dockerized GoTrue → Postgres on another VM. Use Docker pause, DB VM stop, or bridge network policy for meaningful DB-down probes.
  8. Partner OIDC sub claim: id_token.sub may be partner-scoped UUID distinct from auth.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
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-fast 13.9T ~13.8T free; tank-bulk 55.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):

  1. Default: one Postgres per trust boundary (LastVet already separate; Onset separate; last1.id separate; RealOutcomes separate).
  2. Exception: keep last1.app + RadioCheck on one Postgres through Tier 6 cutover; schedule a later split only if product/legal boundaries require it.
  3. 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)

  1. Approve Tier 2 execution plan + Week 1 kickoff date.
  2. last1.id Mode 2A (5–8 weeks).
  3. last1-admin compute (2–4 days).
  4. RealOutcomes full (1–2 weeks).
  5. Onset clinic (multi-week; counsel/SOC 2 aware).
  6. 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 main SHA 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 / *_app split 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/health or 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 → done with 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

  1. Inventory one SendGrid account: authenticated domains, PTR/SPF/DKIM, templates, webhook events.
  2. Confirm RealSend capacity + MATR lessons (reference-matr-sendgrid-realsend-migration-patte).
  3. Warmup calendar per domain (do not cut cold).
  4. Implement provider abstraction or env swap per product (EMAIL_PROVIDER=realsend).
  5. Coordinated cutover weekend; keep SendGrid read-only 14 days.
  6. 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-approve
  • infra-bw-naming-r940-products-approve
  • migrate-n8n-railway-to-r940
  • cloudflare-pages-marketing-migrations (done 2026-08-24; audit: 6/8 already Pages)
  • migrate-last1-admin-compute-to-r940
  • migrate-realoutcomes-railway-supabase-to-r940
  • migrate-last1-id-railway-supabase-to-r940
  • migrate-onset-clinic-supabase-to-r940
  • migrate-last1app-radiocheck-shared-supabase-to-r940
  • batch-sendgrid-to-realsend-last1-domains

Open questions for Ryan

  1. Accept per-product BWS prefixes as written?
  2. Week 1 kickoff date for Tier 2 last1.id Mode 2A (see LAST1_ID_MIGRATION_PLAN.md).
  3. Provide Railway + Supabase billing exports to harden Sub-task F.
  4. Post-Tier 2: Onset Automation OAuth duplicate under last1app GCP 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.