Skip to content

Last 1 Enterprises — Infrastructure Consolidation Plan

Status: Planning only (2026-08-20). Do not execute migrations from this document without Ryan review.
Branch: docs/last1-consolidation-plan (lastvet-gates). Do not merge until Ryan accepts sequencing.
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: Read-only inventory from GitHub + Proxmox + existing gates docs. No Railway/Supabase/SendGrid/BWS mutations. No product code changes.

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 identity critical path RadioCheck edge last1-id-callback, LastVet/Onset OAuth env surface, admin gateway. Treat as high-risk and migrate before or carefully coordinated with dependents.
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 sequence adjustments vs Ryan’s draft tiers: (1) demote standalone monthly-irs-sync; (2) keep n8n as Tier 1 warm-up; (3) allow last1-admin compute in Tier 2 but defer its Supabase until Tier 6; (4) treat last1.app + RadioCheck (+ admin auth DB) as one Tier 6 cutover; (5) keep Onset clinical as Tier 5 SOC 2 driver; (6) run SendGrid→RealSend as parallel batch, not interleaved with DB cutovers.

STOP: After this report, wait for Ryan to pick a start product and approve playbook + BW naming before any provisioning.


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.appout 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.
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
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).
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)
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.
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 draft (kept as spine)

Tier Intent
0 Prerequisites (playbook, BW naming, capacity)
1 Low-risk warm-ups
2 Internal tools
3 Bounded real users
4 Critical identity (last1.id)
5 Clinical / PHI (Onset bundle)
6 Platform apps (last1.app + RadioCheck)

Adjustments

Change Why
Tier 0 now This document + BW naming extension + Muster items. Capacity already green. Still need: Railway/Supabase dashboard export for $ and sizes; RealSend capacity confirmation.
Tier 1 = n8n only monthly-irs-sync is not separable; moving it alone teaches little and risks splitting RealOutcomes.
Tier 2 = last1-admin compute Move Railway Node to R940; keep Supabase auth on tcthzakppndkykatoefx until Tier 6. Reduces Railway spend without DB surgery.
Tier 3 = RealOutcomes including IRS cron Bounded product; own Supabase; good second full-stack rehearsal after n8n.
Tier 4 = last1.id Still high risk. Prefer before Tier 6 (RadioCheck depends on it). Soft-block: document OAuth redirect URI + webhook secret rotation checklist.
Tier 5 = Onset clinic + Onset Supabase (+ mobile env cuts) SOC 2 driver. onset-web stays on Cloudflare.
Tier 6 = last1.app + RadioCheck (+ last1-admin auth DB) Mandatory bundle. Highest coordination cost.
Parallel: SendGrid → RealSend Do not interleave domain warmup with DB cutovers. Batch domains; cut after warmup.

Suggested calendar shape (planning only)

  1. Approve this plan + BW naming.
  2. n8n (1–3 days).
  3. last1-admin compute (2–4 days).
  4. RealOutcomes full (1–2 weeks).
  5. last1.id (2–3 weeks, freeze window).
  6. Onset clinic (multi-week; counsel/SOC 2 aware).
  7. last1.app + RadioCheck 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
  • 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. Accept Tier 1 = n8n only and IRS folded into RealOutcomes?
  3. For Supabase Auth: hybrid (DB on R940, Auth stays Supabase) allowed as interim?
  4. Provide Railway + Supabase billing exports to harden Sub-task F.
  5. Which product starts first after approval?

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.