Skip to content

last1.id Migration Plan — Tier 2 Mode 2A (R940 Sovereign Identity)

Status: Tier 2 CLOSED — 2026-08-26. Sovereign production at https://auth.last1.id. Railway last1.id, hosted Supabase nztgswcpnwggymncrnyx, and legacy GCP OAuth client deleted. BWS cleaned to 33 live keys. Ops: LAST1_ID_MIGRATION_RUNBOOK.md.
Branch: docs/tier-2-last1-id-migration-plan → merged via PR #35
Audit date: 2026-08-24 (Gate A live read: 2026-08-24)
Mode: 2A — self-hosted Postgres + GoTrue + Kong on R940; MinIO for profile photos


Tier 2 closeout summary (2026-08-26)

Phase Outcome Report
Weeks 1–5 Infrastructure, migration, app deploy, OAuth, test matrix W1–W5 reports, Gate A/B/C
Week 6 Cutover to auth.last1.id; 24h soak 99/99 green W6B, W6C
W7-1 Pre-teardown audit: no prod dependency on Railway/Supabase W7-1
W7-2 Archive Railway env + Supabase pg_dump W7-2
W7-3 Delete hosted Supabase nztgswcpnwggymncrnyx W7-3
W7-4 Delete Railway last1.id service W7-4
W7-5 Delete legacy GCP OAuth client (645620802422-…) W7-5
W7-6 BWS cleanup (20 deleted); soak cron removed W7-6
W7-7 / Week 8 Docs, muster, runbook, archive manifest this document + runbook

Archive: backups/last1id-railway-teardown-2026-08-26/ (env, dump, deletion records).

L3 — Kong SPA routing gap (2026-08-27)

Discovered during L2 marketing extraction: https://auth.last1.id/login returned Kong 404. GoTrue (/auth/v1/*) and partner OIDC (/.well-known, /oauth) were routed; the monolith Express SPA (last1id-app:5000) was deployed on R940 but not exposed for /login, /api/*, or static assets.

Sub-task Outcome
L3-1 Kong config Added routes in scripts/last1-id/kong/kong.yml for /api, /assets, and SPA paths → last1id-app:5000. Preserved /auth/v1/*, /rest/v1/*, existing OIDC routes.
L3-2 Apply apply-kong-config.sh applied to auth-last1id VM 114; Kong restart green.
L3-3 Curl matrix /login, /signup, /api/config → 200 HTML/JSON; /api/identity/me → 401 (not 404); GoTrue + PostgREST + /oauth/authorize unchanged.
L3-4 Ryan browser test PASS (2026-08-27) — login/signup render, Google OAuth, settings navigation.
L3-5 Partner OIDC regression PASS — W6B + Week5 session cascade E2E green post-Kong routes.
L2-6 DNS cutover COMPLETE — last1.id + www.last1.id → last1id-web Pages.
L2-7 Monolith cleanup PR COMPLETE — last1.id#114 merged (c332db0); post-merge deploy verified on VM 114.

Post-mortem (honest): Tier 2 closeout validated GoTrue API, partner OIDC scripts, and health endpoints. It did not walk each documented user journey in a browser (marketing CTA → /login → dashboard). W6B Kong work scoped to OIDC paths only; the hostname split docs assumed the full monolith app on auth.last1.id but ingress was never completed. Lesson: for hostname or ingress changes, verify every documented user-facing URL end-to-end in a browser, not only component-boundary checks (GoTrue health, OIDC discovery, synthetic API signup).

Rollback: Restore prior kong.yml from /opt/auth-last1id/volumes/kong/backups/ on VM 114 and run apply-kong-config.sh.

Deferred (explicit — not Tier 2 blockers)

Item Notes
iOS issuer Still https://last1.id in app bundle. Rebuild when Ryan ready; prod auth is auth.last1.id.
my.last.vet Last1 sign-in New product scope; separate muster item.
Provider portal staging issuer Staging env may still reference old issuer; small cleanup task.
BWS duplicate-key hygiene Guard copy-railway-secrets-week3.sh against re-creating duplicate key names.

Ryan decisions locked (2026-08-24)

# Decision
Q1 — Mode Mode 2A — full sovereign identity stack on R940 (Postgres + GoTrue + MinIO). Not Mode 1 interim.
Q2 — Tier Tier 2 = last1.id (moved up from Tier 4). Tier 3 = last1-admin; Tier 4 = RealOutcomes; Tier 5 = Onset; Tier 6 = last1.app + RadioCheck + angel.report.
Q3 — Hostnames Split: auth.last1.id = primary auth service (R940 tunnel). last1.id = marketing on Cloudflare Pages (not Railway app). Partner OIDC issuer = https://auth.last1.id.
Q4 — Google OAuth Dedicated last1.id GCP project (client 889079898378-…). Legacy last1app client deleted W7-5.
Q5 — Soak 24–48 hours (Ryan founder call; 0 production users). Keep Railway + hosted Supabase alive as fallback during soak.

Target state (founding intent)

Layer Sovereign on R940
Compute auth-last1id VM — Node 22, last1.id Docker app
Database sovereign-db-last1id VM — Postgres 17.6.x + pgcrypto
Auth service GoTrue (self-hosted) + Kong API gateway
Object storage MinIO — profile-photos bucket
Ingress Cloudflare Tunnel → auth.last1.id
Marketing Cloudflare Pages → last1.id
Partner OIDC Custom layer unchanged; OAUTH_JWT_* PEM preserved

Y1 — Service inventory (Ryan review checkpoint)

Repo

Item Finding
GitHub Last-1-Enterprises/last1.id
Branch main @ 358f083 (2026-08-10 push)
Language TypeScript (strict), Node 22
Backend Express 5 (server/index.ts)
Frontend React 19 + Vite 8 + Wouter + TanStack Query
Build script/build.ts → dist/public (SPA) + dist/index.cjs (API)
Packages packages/sdk, sdk-nextjs, sdk-react-native, login-button, evidence-sdk
Migrations 71 SQL files in supabase/migrations/ (~2170 lines)

Railway (live, read via GraphQL API)

Item Value
Project disciplined-beauty (0cb8d8fa-5216-479a-b1f5-00d2dfa7c88b)
Service last1.id (b84596ac-82b1-40b2-96ab-f4e4ec03a186)
Environment production (12ba2dc7-8e11-4ab4-a4d9-9ecfecb47533)
Service instance 5ed12f59-f57f-499b-add0-beb3cee55413
Created 2026-03-15
Custom domains last1.id, www.last1.id
Replicas Not exposed via API (Railway default)
Memory/CPU Unknown — not available via GraphQL; confirm in Railway UI
Deploy pattern Dockerfile multi-stage (node:22-alpine), port 5000
Start command node dist/index.cjs
Health check /api/health, 30s timeout, restart on_failure ×10
Railway env var names Not enumerable via API in this audit; names documented from repo below

Supabase

Item Value
Project ref nztgswcpnwggymncrnyx (linked in package.json, supabase/config.toml)
Postgres version 17.6.x (per consolidation plan .temp/postgres-version)
Auth Supabase GoTrue (hosted) — not self-hosted
Storage Enabled (profile photos, exports)
RLS Enabled on sensitive public tables

Current URLs

URL Role
https://last1.id Today: Railway app + OIDC issuer → After cutover: Cloudflare Pages (marketing only)
https://www.last1.id Marketing alias (Cloudflare Pages)
https://auth.last1.id Primary auth service + OIDC issuer (R940 via Cloudflare Tunnel)

Deployment pattern

  1. Git push → Railway builds Dockerfile with build-args VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY
  2. Single Node process serves API + static SPA in production
  3. Supabase hosts Postgres + GoTrue + Storage (external dependency)
  4. In-process jobs: maintenance cleanup, webhook drainer (WEBHOOK_DRAINER_ENABLED)

Environment variable names (from repo; no values)

Required in production (server/lib/envValidation.ts + .env.example):

Category Names
Core NODE_ENV, PORT, SITE_URL, CORS_ORIGINS
Supabase SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY, SUPABASE_DB_PASSWORD, DATABASE_URL, SUPABASE_ACCESS_TOKEN
OIDC issuer (last1.id as IdP) OAUTH_JWT_PRIVATE_KEY, OAUTH_JWT_PUBLIC_KEY, OAUTH_JWT_KID, LAST1_ID_ISSUER_URL, PUBLIC_BASE_URL, OAUTH_AUDIENCE
Dev-only OIDC fallback OAUTH_JWT_SECRET, OAUTH_ALLOW_HS256_FALLBACK
Credential / VC signing CREDENTIAL_SIGNING_KEY, CREDENTIAL_SIGNING_KEY_PREVIOUS
Trust ingestion LASTVET_APP_SECRET, LAST1_APP_SECRET, RADIOCHECK_APP_SECRET, MATR_IOS_APP_SECRET, MATR_BIZ_APP_SECRET (+ dynamic {APP_ID}_APP_SECRET)
Admin gateway ADMIN_GATEWAY_SECRET, LAST1_ADMIN_GATEWAY_SECRET, LAST1ID_ADMIN_GATEWAY_SECRET
Optional Turnstile, Twilio Verify, SendGrid, Sentry, maintenance alert webhooks, compliance bundle secrets, webhook drainer tuning

Not in repo (Supabase Dashboard): Google/Microsoft/ID.me OAuth client IDs and secrets for end-user login.


Y2 — Auth method audit (current vs target)

Ryan-approved target: universal SSO IdP with Google + Apple + email + magic link + passkey.

Two auth layers (architecture)

Layer Purpose Implementation
A. User login to last1.id Dashboard, developer portal, consent UI Supabase GoTrue via @supabase/supabase-js
B. Partner "Sign in with Last1 ID" LastVet, RadioCheck, Onset, etc. Custom OAuth 2.0 / OIDC (server/routes/oauth.ts)

Layer A — end-user auth (Supabase)

Method Current state Target Notes
Email + password Working Keep login.tsx / signup.tsx
Google OAuth Working Keep Supabase provider; credentials in Supabase Dashboard only
Email confirmation / reset Working Keep Standard Supabase flows
Magic link (OTP login) Not implemented Add No signInWithOtp in codebase
Apple Sign-In Not implemented Add No Apple provider code or env vars
Passkey / WebAuthn Not implemented Add No WebAuthn references
Microsoft Stub UI TBD Button disabled "Coming soon"; docs say enable in Supabase
ID.me Stub UI TBD Same as Microsoft
MFA / 2FA None TBD No Supabase MFA API usage

Layer B — last1.id as OAuth/OIDC IdP (partner apps)

Capability Current state
OIDC discovery /.well-known/openid-configuration
JWKS /.well-known/jwks.json
Authorization code + PKCE Working
Refresh tokens oauth_refresh_tokens table
Token revoke POST /oauth/revoke
Silent auth (prompt=none) Server-side only
Partner webhooks Outbound HMAC (app_clients.webhook_*)
Trust events POST /api/trust-events(/batch)

JWT signing

Token type Algorithm Key management
OIDC id_tokens (issued by last1.id) RS256 (prod required) OAUTH_JWT_PRIVATE_KEY, OAUTH_JWT_PUBLIC_KEY, OAUTH_JWT_KID
Supabase session JWTs (user login) Supabase-managed Keys in Supabase project; app validates via supabase.auth.getUser()

Migration critical path: OAUTH_JWT_* PEM keys must be copied byte-identically to R940 or all partner OIDC tokens break. Supabase JWT verification continues to use Supabase JWKS until auth stack moves.

Session pattern

Session Storage
last1.id user (Supabase) Client-side Supabase session (localStorage default)
Partner OAuth refresh tokens Postgres oauth_refresh_tokens
Server-side cookie session None (explicit in oauth routes)
OAuth handshake state Browser sessionStorage

Zero users today: session invalidation on cutover is acceptable; document for future migrations.


Y3 — Dependency mapping

In-workspace integrations (verified code)

Product Integration surface Key env / config Breaks if issuer URL changes?
last.vet-api OAuth client, JWKS verify, trust events OUT, webhooks IN, Onset proxy LAST1_ID_API_BASE, LASTVET_APP_SECRET, redirect URIs, LAST1_WEBHOOK_SECRET Yes — iss must match base URL
last.vet-provider Browser OAuth to /oauth/authorize NEXT_PUBLIC_LAST1_ID_ISSUER (build-time) Yes — rebuild required
last.vet-ios PKCE OAuth Hardcoded https://last1.id in Configuration.swift Yes — App Store rebuild
last.vet-web Universal Link bridge only /auth/last1/callback → lastvet:// No (redirect URI unchanged)

Canonical spec: last.vet-api/docs/api/LAST1_ID_INTEGRATION.md

Out-of-workspace (documented; repos not cloned)

Product Integration Redirect / callback examples
RadioCheck OAuth + trust events + revoke webhook radiocheck://last1-id/callback; Supabase edge last1-id-callback
Onset OAuth + Connect assertion onset://last1/callback, https://theonset.app/last1/callback
last1-admin API gateway proxy VITE_LAST1_API_BASE
last1.app Planned SSO alignment Phase 2 roadmap
RealOutcomes Weak / admin-mediated Optional cert registry

Registered OAuth clients (DB seeds, not env)

From migrations: last1, radiocheck, matr-ios, matr-biz, realoutcomes, lastvet, onset.

Partner redirect URIs do NOT include the last1.id hostname (they point at product URLs). Migration changes issuer/authorize/token/JWKS host, not partner callback paths.

What breaks on last1.id → auth.last1.id

Breaks Does not break
All LAST1_ID_API_BASE / issuer env vars https://provider.last.vet/auth/callback
iOS hardcoded issuer https://last.vet/auth/last1/callback
OIDC iss claim validation onset://last1/callback
JWKS URL if derived from issuer Partner webhook URLs on api.last.vet
Supabase Auth Site URL / redirect allowlist {APP_ID}_APP_SECRET values (unless rotated)
Google authorized origins (if tied to last1.id)

Y4 — Database schema audit

Live row counts

Gate A (2026-08-24): Read-only PostgREST + GoTrue admin audit completed. Not zero users: 12 auth.users, 543 public.* rows, 3 profile-photos objects. Full auth.* / storage.* / pg_policies inventory pending Postgres credential (see LAST1_ID_GATE_A_REPORT.md). Y4 “zero users” = no production veteran traffic; pentest/synthetic/founder accounts present.

Schema (from 71 migrations)

Auth-critical public tables:

Table Purpose
identities Canonical identity UUID
linked_auth_providers Sign-in methods linked to identity
identities.auth_user_id FK → auth.users (Supabase managed)
app_clients OAuth client registry + redirect URIs + webhook config
identity_app_links Identity ↔ partner app linkage
consent_grants Scoped consent
oauth_authorization_codes Auth codes (PKCE fields)
oauth_refresh_tokens Partner refresh token chains
oauth_token_audit_log Token exchange audit
consent_audit_log Consent audit
webhook_deliveries Outbound webhook outbox
trust_events Cross-product trust ledger
credentials, issuer_keys, … VC / compliance surfaces

Supabase-managed (not in public migrations): auth.users, auth.sessions, auth.identities, refresh token tables internal to GoTrue.

Total public tables: ~35 (see consolidation plan inventory).

Sensitive data categories

Data Location Migration note
Password hashes auth.users (Supabase) Requires GoTrue migration or Supabase export
OAuth provider tokens Supabase auth schema Supabase-managed
Partner refresh tokens oauth_refresh_tokens.token_hash Migrate with Postgres dump
OIDC signing keys Railway env OAUTH_JWT_* Copy via BWS; never commit
Credential issuer keys issuer_keys (encrypted with CREDENTIAL_SIGNING_KEY) DB + KEK must move together
App client secrets BWS + Railway ({APP}_APP_SECRET) Reconcile both sides

Current backup strategy

Layer Finding
Supabase Platform PITR/backups (plan-dependent); no custom runbook found in last1.id repo
Railway Stateless compute; state in Supabase
Application exports Identity export API exists; not a DR substitute

Execution requirement: Full logical backup (pg_dump including auth schema) + verified restore on R940 before any destructive step. Follow Tier 3+ export hygiene: gitignore archives, no plaintext secrets in git.


Y5 — OAuth callback registration audit

A. End-user federated login (Supabase → Google)

Console What to update for auth.last1.id
Supabase Dashboard → Authentication → URL Configuration Site URL, Redirect URLs
Google Cloud Console → OAuth client used by Supabase Authorized JavaScript origins (https://auth.last1.id), authorized redirect URI remains Supabase callback (https://<project-ref>.supabase.co/auth/v1/callback)

Google client ID: Not in last1.id repo. Lives in Supabase Dashboard → Authentication → Providers → Google.

Apple: Not configured. When added, requires Apple Developer Services ID + Supabase Apple provider + domain verification for auth.last1.id.

B. last1.id as IdP (partner OAuth)

Registered in app_clients.redirect_uris (Postgres). Production URIs from migrations:

App Production redirect URIs
lastvet https://provider.last.vet/auth/callback, https://last.vet/auth/callback
onset onset://last1/callback, https://theonset.app/last1/callback
radiocheck radiocheck://last1-id/callback, radiocheck://callback, HTTPS variants per migrations
last1 https://last1.id/dashboard, https://last1.app/api/auth/last1/callback, …
realoutcomes https://realoutcomes.app/
matr-* Custom scheme + localhost dev URIs

Post-migration: Update last1 app client URIs if any reference https://last1.id/... paths on the IdP host. Partner product URIs unchanged.

C. Issuer metadata (must change if hostname changes)

Set on R940:

  • SITE_URL=https://auth.last1.id
  • PUBLIC_BASE_URL / LAST1_ID_ISSUER_URL=https://auth.last1.id
  • OIDC discovery + JWKS served from new host
  • Consider keeping last1.id as 301 → auth.last1.id during transition (Open Question 3)

Y6 — Migration playbook draft

Extends Tier 1 n8n pattern with identity gates. Two execution modes (recommend Mode 1 first):

Mode Scope Risk
Mode 1 — Compute only Railway → R940; keep Supabase hosted Medium
Mode 2 — Full stack Compute + Supabase → self-hosted Postgres/GoTrue on R940 High

Ryan's task mentions sovereign-db VM; Mode 2 aligns with long-term consolidation plan. Do not combine Mode 2 with auth method expansion in the same cutover.

Phase 0 — Preconditions (sequential)

Step Owner Blocker
Ryan approves this plan + open questions Ryan Yes
Export archive policy (gitignored JSON/metadata only) Cursor Tier 1 lesson
BWS keys for R940 prod mapped (LAST1ID_R940_PROD_* or similar) Ryan Naming approval
Observability design for last1.id (Prometheus, health, JWT expiry alerts) Cursor Parallel

Phase 1 — Pre-migration (parallel where noted)

# Step Sequential? Notes
1.1 Provision R940 VM (last1id-r940 or consolidation name) Sequential Docker, node 22, systemd, ufw
1.2 Cloudflare tunnel route auth.last1.id → VM (DNS-only or proxied per policy) After 1.1 Mirror n8n lastvet-edge pattern
1.3 Full Supabase logical backup + restore test to scratch Sequential Includes auth schema
1.4 Copy signing material to BWS (OAUTH_JWT_*, CREDENTIAL_SIGNING_KEY, app secrets) Parallel with 1.3 Byte-identical PEM
1.5 Document live Railway env var names snapshot (UI export) Parallel No values in git
1.6 Register R940 instance in observability After 1.1 Tier 1 pattern

Phase 2 — Database (Mode 2 only; skip for Mode 1)

# Step Notes
2.1 Provision Postgres VM (sovereign-db-* per consolidation plan) Dedicated instance; not shared with n8n
2.2 Restore Supabase dump OR logical replication High risk — GoTrue compatibility
2.3 Self-host GoTrue / Supabase Auth stack OR replace auth layer Major engineering — not scripted in repo today
2.4 Point SUPABASE_URL at self-hosted OR refactor off Supabase SDK Likely multi-week project

Recommendation: Defer Phase 2 until Mode 1 is stable. Consolidation plan already treats last1.id Postgres as dedicated trust boundary.

Phase 3 — Application deployment (R940)

# Step Notes
3.1 Build Docker image from main with same build-args Match Railway Dockerfile
3.2 Deploy with production env from BWS SITE_URL, issuer URLs, Supabase keys
3.3 Run health checks: /api/health, OIDC discovery, JWKS Automated smoke script
3.4 Parallel-run on private hostname (last1id-r940.tailc3bbdc.ts.net) No public traffic

Phase 4 — OAuth / provider console updates (before public cutover)

# Step Must precede DNS
4.1 Update Supabase Auth Site URL + redirect allowlist Yes
4.2 Update Google Cloud authorized origins for new SPA host Yes
4.3 Update app_clients rows if any IdP-host redirect URIs Yes
4.4 Notify partner env updates (LAST1_ID_API_BASE, iOS rebuild) Before dependent testing
4.5 Apple / passkey consoles Post-expansion only

Phase 5 — Parallel run + cutover test

# Step Notes
5.1 Railway stays live; R940 serves tailnet + synthetic tests Both instances alive
5.2 Run LastVet provider OAuth E2E against R940 issuer (staging env) PKCE + token + JWKS
5.3 Run trust-event batch dry-run / test app LASTVET_APP_SECRET
5.4 Run webhook delivery test to api.last.vet/api/webhooks/last1 HMAC verify

Phase 6 — Traffic cutover

# Step Notes
6.1 Lower DNS TTL for auth.last1.id (and last1.id if redirecting) 24h before
6.2 Switch auth.last1.id CNAME to Cloudflare tunnel Primary cutover
6.3 Optional: last1.id → 301 to auth.last1.id Preserves bookmarks
6.4 Update all partner LAST1_ID_API_BASE / issuer envs Coordinated deploy
6.5 Deploy LastVet iOS with updated issuer App Store latency

Phase 7 — Soak (7 days minimum)

Check Frequency
OIDC token issuance + JWKS rotation window Daily
LastVet link/unlink + trust drainer Daily
Webhook delivery lag Alert-driven
Supabase connectivity from R940 Continuous
Error budget / Sentry Continuous

Not Tier 1 skip-soak: identity outage breaks all dependents.

Phase 8 — Railway teardown

Step Notes
Deactivate Railway last1.id service After soak
Verify cost delta ~TBD/month
Archive env snapshot (gitignored) Tier 1 pattern
railway-teardown.sh or GraphQL delete BWS token ready

Y7 — Auth method expansion sequencing

Rule: Preserve current behavior through migration; expand after R940 is stable.

Method When to implement Rationale
Email + password Migrate as-is Already on Supabase
Google OAuth Migrate as-is Supabase provider config moves with Site URL
Magic link After migration New Supabase feature wiring
Apple Sign-In After migration New provider + Apple Developer setup
Passkey / WebAuthn After migration New feature; likely Supabase WebAuthn or custom
Microsoft / ID.me Defer Stub UI only

Do not add Apple + passkey + hostname change + DB migration in one release.


Y8 — Risk register (Mode 2A execution)

Risk Likelihood Impact Mitigation
Partner OIDC JWT key rotation Medium Critical — LastVet link/sign-in breaks Preserve OAUTH_JWT_PRIVATE_KEY, OAUTH_JWT_PUBLIC_KEY, OAUTH_JWT_KID byte-identically via BWS; verify JWKS match before cutover
Google OAuth callback mismatch Medium High — user login fails Update GCP Last1.id client before DNS cutover: add origin https://auth.last1.id, callback https://auth.last1.id/auth/v1/callback; keep hosted Supabase callback during parallel run
LastVet integration break Medium High PEM continuity + synthetic-account OIDC test in Week 5; coordinate LAST1_ID_API_BASE=https://auth.last1.id deploy with cutover
MinIO profile photo loss Medium Medium Export storage.objects + verify every profile-photos object in MinIO before Supabase Storage delete; checksum spot-check
Database migration data loss Low (0 users) Critical if wrong Full pg_dump (auth+public+storage schemas); restore drill Week 2; keep hosted Supabase alive through 24–48h soak
Issuer URL / iss mismatch High Critical Set SITE_URL, PUBLIC_BASE_URL, LAST1_ID_ISSUER_URL to https://auth.last1.id; update LastVet provider + iOS builds
GoTrue SMTP misconfig Medium Medium — reset/confirm emails fail Provision SMTP_* in BWS before Week 3; test password reset on staging
Railway fallback removed too early Medium High Week 6: deactivate schedules only first; delete Railway service Week 7–8 after soak
Onset Automation OAuth duplicate (GCP) Low Low (hygiene) Duplicate OAuth client under last1app — defer to post-Tier 2 GCP cleanup
Export archive secret leak Medium Medium Gitignore migration dumps; encrypted off-repo only
CREDENTIAL_SIGNING_KEY rotation Low High Never rotate during migration window

Y9 — Effort estimate (summary)

Estimates are active engineering hours (Ryan gate reviews add calendar time). Detailed Mode 2 breakdown: Y11.

Mode 1 — Compute migration only (keep Supabase hosted)

Workstream Hours
Planning approval + BWS naming 4–8
VM + tunnel + observability 8–12
BWS env mapping + deploy scripts 8–12
Staging parallel run + OAuth console updates 8–12
LastVet E2E + partner smoke tests 12–16
Cutover + 7-day soak (monitoring) 8–12 active + 7 calendar days
Railway teardown + docs 4–6
Total active 52–78 hours (~2–3 weeks with reviews)

Sovereignty: compute on R940 only. Identity data and auth JWT signing keys remain on Supabase SaaS.

Mode 2 — Full self-host (target state)

Variant Active hours Calendar (1 FTE + Ryan gates) Sovereignty
2A — Self-hosted Postgres + GoTrue + MinIO (recommended) 140–200 5–8 weeks Data + auth + object storage on R940
1.5 — Self-hosted Postgres + Better Auth 180–260 6–10 weeks Data sovereign; drops Supabase SDK; moderate rewrite
2B — Ory Kratos/Hydra or custom 320–520 4–7 months Full control; largest rewrite

The earlier "2–4 months / multi-month" range applied to 2B-style auth replacement, not to 2A. There is a credible 5–8 week sovereign path if Ryan chooses self-hosted Supabase (GoTrue + Postgres + Storage) without replacing @supabase/supabase-js.


Y11 — Mode 2 detailed effort breakdown

Audit basis: last1.id @main shallow clone; 88 TypeScript files touch Supabase auth; server uses service role for DB (RLS enabled, bypassed via service_role policies); client login is 100% Supabase SDK; partner OIDC is custom Express (~1042 lines oauth.ts, unchanged by GoTrue host move).

1. GoTrue vs custom auth decision

last1.id today has two auth systems:

System Role Migration sensitivity
Supabase GoTrue End-user login (email, Google, sessions) Host move or library swap
Custom OIDC (OAUTH_JWT_*) Partner "Sign in with Last1 ID" PEM key continuity only; not GoTrue

Option comparison

Option Description One-time hours Ongoing maintenance Keeps current code?
2A — Self-host Supabase stack Official Docker: Postgres + GoTrue + Storage API + Kong (+ optional Studio). Point SUPABASE_URL at R940. 40–64 (infra + config) 2–4 h/month ops (upgrades, backups, cert rotation) Yes — @supabase/supabase-js unchanged
2A-lite — GoTrue + Postgres only Self-host GoTrue + Postgres; replace Storage with MinIO adapter 48–72 3–5 h/month Mostly yes; Storage API swap for profile photos
1.5 — Better Auth in Express Embed Better Auth in existing Node server; Postgres sessions 80–120 auth layer 2–3 h/month Partial — rewrite client login + getAuthUserFromRequest
1.5 — Auth.js (NextAuth v5) Similar to Better Auth; less natural fit for Express monolith 90–130 2–3 h/month Partial
2B — Ory Kratos + Hydra Enterprise IAM split; replace GoTrue + part of OIDC 200–320 4–8 h/month No — major rewrite
2C — Custom auth Build email/OAuth/session from scratch 400–600+ 8–16 h/month No

Can we self-host GoTrue? Yes. GoTrue is open source (Apache 2.0). Supabase runs it; you can run the same binary via supabase/supabase self-hosting docs or a trimmed compose with supabase/gotrue, Postgres, and Storage.

Recommendation for sovereign founding intent: Mode 2A (full self-hosted Supabase stack on R940 sovereign-db-* VM + app on last1id-r940). Delivers data sovereignty and operational control without a 3-month auth rewrite. Auth method expansion (Apple, passkey, magic link) then layers on GoTrue features post-cutover.

Not recommended for speed: Ory/custom (2B/2C) unless Supabase self-host is rejected on policy grounds (e.g. refuse to run Kong/GoTrue stack at all).


2. Database migration real scope

Extensions (from 71 migrations)

Extension Required? Notes
pgcrypto Yes Used in 6 migrations (issuer keys, org admin, webhooks)
uuid-ossp / pgcrypto gen_random_uuid Implicit UUID PKs throughout
pgvector No Not referenced in migrations
postgis No Not referenced

auth schema (Supabase-managed; must migrate with GoTrue)

Typical GoTrue tables to include in dump/restore (exact set depends on Supabase version):

Table Purpose
auth.users Email, password hash, metadata, confirmation state
auth.identities Federated provider links (Google sub, etc.)
auth.sessions Active refresh/session chains
auth.refresh_tokens GoTrue refresh token storage
auth.mfa_factors MFA (empty today; schema may exist)
auth.mfa_challenges MFA challenges
auth.mfa_amr_claims MFA AMR
auth.audit_log_entries Auth audit trail
auth.flow_state OAuth PKCE / SSO flow state
auth.one_time_tokens Email confirm, password reset tokens
auth.sso_providers / auth.sso_domains / auth.saml_providers SSO (likely empty)

FK bridge: public.identities.auth_user_id → auth.users(id) (20260314100000_auth_and_onboarding.sql).

public schema (~35 application tables)

Auth-critical subset:

identities, linked_auth_providers, app_clients, identity_app_links, consent_grants, oauth_authorization_codes, oauth_refresh_tokens, oauth_token_audit_log, consent_audit_log, webhook_deliveries, trust_events, plus org/admin/VC tables.

Full table list in Y4 audit; all defined in supabase/migrations/.

storage schema

Object Purpose
storage.buckets profile-photos bucket seeded in migration
storage.objects Profile photo blobs

App uses supabase.storage.from("profile-photos") in identity.ts and profilePhotoMirror.ts.

RLS policies

Metric Count
Tables with ENABLE ROW LEVEL SECURITY ~32 public tables
RLS-related migration statements ~74 lines
Effective access pattern Server uses service role (bypasses RLS); anon/authenticated denied by default

Migration work: RLS definitions migrate with schema via pg_dump; verify service_role policies after restore. No RLS redesign required unless dropping Supabase roles.

Data migration hours (schema + data only)

Task Hours
Pre-migration logical backup from hosted Supabase (pg_dump -Fc auth+public+storage) 4–8
Provision Postgres 17 on R940 (VM, disk, ufw, backup job) 12–16
Restore drill to scratch instance + migration replay test 8–12
Row-count / FK integrity validation scripts 4–8
Storage object export/import (profile photos) 8–16
Cutover window execution (read-only freeze, final dump, restore) 4–8
Post-cutover smoke queries 4–6
Subtotal (DB only) 44–74 hours

With zero users (Ryan): data volume is trivial; hours are dominated by process and verification, not transfer size.


3. OAuth flow reimplementation

Flows that exist today

Flow Implementation Self-host impact
Email signup/login supabase.auth.signUp / signInWithPassword GoTrue config + SMTP (Supabase handles mail hooks)
Google login supabase.auth.signInWithOAuth({ provider: "google" }) Re-enter Google client in GoTrue env; Supabase callback URL changes to self-hosted auth URL
Password reset resetPasswordForEmail GoTrue mail templates + Site URL
Email confirmation signup emailRedirectTo Site URL on self-hosted auth
Partner OIDC authorize/token/revoke Custom server/routes/oauth.ts No reimplementation if Postgres + signing keys move intact
Legacy /authorize bridge SPA + identity.ts Unchanged
Trust events / webhooks Bearer {APP}_APP_SECRET Unchanged

Supabase-specific SDK usage to replace (only if not using 2A)

Usage Files Replace effort (1.5 only)
Client auth session AuthContext.tsx, login.tsx, signup.tsx, … 24–40 h
Server auth.getUser(token) server/lib/auth.ts + all protected routes 16–24 h
Server auth.admin.deleteUser identityDeletion.ts 4–8 h
Storage profile photos identity.ts, profilePhotoMirror.ts 16–24 h (MinIO/S3 API)
createClient service role (~20 route modules) Can keep postgres driver OR keep supabase-js pointed at self-hosted 0 h (2A) / 8–16 h (direct pg)

Mode 2A hours (OAuth reconfiguration, not reimplementation): 12–20

Task Hours
GoTrue Google provider env + Google Cloud console origins 4–8
GoTrue Site URL / redirect allowlist for auth.last1.id 2–4
SMTP for auth emails (if not using Supabase hosted mail) 4–8
Regression: login, signup, reset, Google round-trip 4–8

Mode 1.5 hours (SDK replacement): 60–96 (see table above)


4. Session management

Aspect Current (Supabase) Mode 2A (self-host GoTrue) Mode 1.5 (Better Auth)
User session JWT access + refresh in client localStorage via SDK Same pattern; JWT signed by your GoTrue JWT secret Library-managed session cookies/tokens
API auth Authorization: Bearer <supabase_access_token> → getUser() Same code path; different JWKS endpoint Rewrite middleware
Partner sessions oauth_refresh_tokens table (app-issued) Unchanged Unchanged
Server cookie session None None Optional in Better Auth

JWT continuity concerns:

JWT Must preserve on cutover?
OAUTH_JWT_PRIVATE_KEY (partner OIDC) Yes — copy PEM exactly
GoTrue JWT secret (user sessions) No impact today (0 users); can rotate if desired
Supabase anon/service keys Regenerate for self-hosted project

Hours:

Task 2A 1.5
Export/import GoTrue JWT secret + configure 2–4 N/A
Session smoke tests (login → API call → refresh) 4–8 8–12
Document session invalidation policy 2 2
Subtotal 8–14 10–14

5. Testing across auth methods

Current methods to test (migration scope)

Method Automated coverage today Manual E2E needed
Email/password login Vitest integration tests exist (identity.*.int.test.ts, ~59 test files repo-wide) Yes — full browser flow
Email signup + confirm Partial Yes
Password reset None found Yes
Google OAuth None in CI (needs real Google) Yes — required
Partner OIDC (LastVet) API integration tests Yes — provider portal + API
Trust events / webhooks Integration tests Smoke on cutover

Testing hours

Task Hours
Extend CI against self-hosted Supabase (docker compose in GitHub Actions or R940 staging) 12–16
Manual E2E matrix: email login, signup, reset 8–12
Google OAuth E2E on staging auth.last1.id 4–8
Partner OIDC: LastVet provider + API token exchange + JWKS 12–16
Webhook delivery + trust-event batch 4–8
Load/soak monitoring scripts (24–48h soak ops) 4–8
Subtotal 44–68

Post-migration auth expansion (Apple, magic link, passkey) is out of scope for Mode 2 cutover testing; budget separately (+40–80 h per method).


Component Hours
A. Infrastructure
Postgres VM (sovereign-db-last1id) + backups + monitoring 16–24
Self-hosted Supabase stack (GoTrue + Kong) + MinIO 24–40
App VM (auth-last1id) + Cloudflare tunnel auth.last1.id 12–16
BWS secret mapping + deploy systemd/docker 8–12
B. Database migration 44–74
C. OAuth / GoTrue config 12–20
D. Session / JWT continuity 8–14
E. Application deploy + parallel run 12–16
F. Partner coordination (LastVet env, iOS issuer, provider rebuild) 16–24
G. Testing (section 5) 44–68
H. Cutover + 24–48h soak (active monitoring) 8–12
I. Teardown (Railway + hosted Supabase project after verification) 6–10
J. Runbook + observability 8–12
Total active 212–316

Realistic planning number: ~240 hours (6 weeks at 40 h/week, or 5–8 calendar weeks with Ryan gate reviews and 24–48h soak wall time).

Not multi-month unless Ryan chooses 1.5/2B auth library replacement.


Mode 1.5 hybrid analysis (Postgres self-host + Better Auth)

Concept: Migrate compute to R940, self-host Postgres with identity data, replace Supabase GoTrue with Better Auth embedded in existing Express app. Keep custom partner OIDC layer (oauth.ts) as-is.

Advantage Disadvantage
Data on R940 60–96 h auth layer rewrite
No Supabase SaaS dependency Must reimplement Google OAuth, email flows in Better Auth
Cleaner long-term than operating full Supabase stack Profile photo Storage → MinIO migration
Still "sovereign identity data" Loses Supabase Dashboard for auth provider toggles
Component Hours
Everything in Mode 2A infra + DB (minus Supabase stack) 60–90
Better Auth integration (server + client) 80–120
Storage → MinIO for profile photos 16–24
Testing 40–56
Total 196–290 (~6–10 weeks)

Verdict: Mode 1.5 is similar calendar time to 2A but more code risk. Prefer 2A unless policy rejects running GoTrue/Kong. Mode 1.5 wins if Ryan wants to exit Supabase-branded stack entirely while avoiding Ory-scale complexity.


Mode comparison — what unblocks what

Milestone Mode 1 (52–78 h) Mode 2A (140–200 h) Mode 1.5 (180–260 h)
Compute off Railway ✅ ~2 weeks ✅ ~2 weeks (parallel with DB) ✅ ~3 weeks
Identity data off Supabase SaaS ❌ ✅ ✅
Sovereign founding story complete ❌ (data on vendor) ✅ ✅
Tier 3–6 identity-dependent cuts safe Partial (vendor DB remains) ✅ ✅
@supabase/supabase-js dependency Keeps Keeps (self-hosted) Removes
Auth expansion (Apple/passkey) After either mode After 2A stable After 1.5 stable
Ongoing vendor cost (Supabase) ~$25+/mo continues $0 after teardown $0

Tier 3–6 blocked without last1.id migration:

Tier Blocker if last1.id stays on Railway+Supabase
3 last1-admin Low direct blocker
4 RealOutcomes Low direct blocker
5 Onset OAuth + Connect assertion depend on issuer
6 last1.app + RadioCheck + angel.report Hard dependency on last1.id OAuth + trust events

Mode 1 interim: Unblocks compute cost and Railway reduction in weeks, but does not complete sovereignty. Ryan has rejected that as end state.

Tier 3–6 lessons (from Tier 2 Week 4 correction)

  1. Auth-gated credentials: When credentials live in a Google Doc, 1Password item, or other location Cursor cannot read, STOP and ask Ryan. Do not infer from generic GOOGLE_OAUTH_* keys in unrelated BWS projects (last1-admin, lastvet, etc.).
  2. BWS project ownership: Product-prefixed keys belong in the product's BWS project. LAST1_ID_R940_* → Last1.id project (cffb320a…); LASTVET_R940_* → lastvet project. Cross-product inference is a rotation and blast-radius failure mode.

Updated recommendation (post Ryan sovereignty confirmation)

Target: Mode 2A direct (skip Mode 1 interim)
Timeline: 5–8 calendar weeks (~240 active hours)
Critical path: Postgres restore drill → GoTrue Google config → partner OIDC smoke → DNS cutover → 24–48h soak
Defer: Apple, magic link, passkey until 30 days post-cutover
Reject for speed: Mode 1 as final state; Ory/custom auth (2B/2C) unless 2A fails policy review
Consider: Mode 1.5 only if GoTrue self-host is unacceptable but Better Auth is approved

X — Mode 2A week-by-week execution plan

Calendar: 5–8 weeks · ~240 active hours · Ryan gate at Week 1 kickoff and Week 6 cutover.

Week Phase Hours Deliverables
1 Infrastructure setup 60–92 See checklist below
2–3 Database migration 44–74 Self-hosted Postgres populated; RLS verified
3–4 Application + OAuth deployment 48–74 GoTrue + app on R940; GCP callbacks staged
5 Testing 44–68 Email, Google, LastVet OIDC green
6 Cutover + soak 8–12 active + 24–48h wall DNS split; Railway deactivated (not deleted)
7–8 Teardown + playbook 14–22 Railway + Supabase delete; docs updated

Week 1 — Infrastructure setup (60–92 h)

Task Notes
Provision sovereign-db-last1id VM on R940 Postgres 17.6.x, pgcrypto, backups, Prometheus node exporter
Provision auth-last1id VM on R940 Node 22, Docker: GoTrue + Kong; systemd for last1.id app
BWS lane (document only until Ryan approves Week 1) Project prefix LAST1_ID_R940_PROD_* — see X-BWS below
Cloudflare Tunnel auth.last1.id → auth-last1id VM
DNS auth.last1.id CNAME/tunnel route (staging hostname OK before cutover)
MinIO on R940 Bucket profile-photos; access keys → BWS (Week 1 execution)
Observability Prometheus scrape + Loki ship (mirror n8n pattern)

Gate: VMs healthy; tunnel returns 502/404 from placeholder (not DNS failure).

Weeks 2–3 — Database migration (44–74 h)

Week 2 gates: A pre-migration audit · B dry-run dump/restore + MinIO · C cutover readiness. All complete 2026-08-25.

Task Notes
Export hosted Supabase schema + data Done via pooler aws-1-us-east-1.pooler.supabase.com (see Gate B)
Import to self-hosted Postgres last1id_prod on VM 113; gate-b-dump.sh / gate-b-restore.sh
Migrate RLS policies 35 live policies; restored with dump
Verify FK integrity 0 orphan identities.auth_user_id
Migrate profile-photos to MinIO 3/3 objects; SHA-256 verified
GoTrue READ-READY v2.196.0; signup/signin/refresh/admin API green (Gate C)

Week 2 totals: 12 auth users, 543 public rows, 3 storage objects, 35 RLS policies. Hosted Supabase + Railway unchanged.

Gate C outcome: sovereign-db-last1id READ-READY for Week 3 app deploy. STOP for Ryan Week 3 approval.

Original Week 2 gate (met): Row counts match; RLS restored; MinIO object count = Supabase Storage count.

Weeks 3–4 — Application + OAuth deployment (48–74 h)

Week 3 outcome (2026-08-25): last1.id app on VM 114 (http://192.168.1.96:5000, LAN/Tailscale only). PostgREST + Caddy TLS bridge + BWS partner secrets. JWKS + LastVet client validation green. STOP for Ryan Week 4 approval. Report: LAST1_ID_WEEK3_REPORT.md.

Task Status
Deploy PostgREST + Kong /rest/v1 Done
Deploy last1.id app (358f083) Done — /opt/last1.id/
Copy OAUTH_JWT_* from Railway → BWS Done — PEM decode + SHA-256 audit
Partner env (LastVet, Turnstile, Twilio, KEK) Done
GOOGLE_OAUTH_*, SMTP_* Not in Railway env; Week 4 BWS + GoTrue (dedicated GCP client)
Public DNS Not done (Week 6)
GCP OAuth (dedicated project) Done Week 4 (old last1app client kept for parallel run)

Week 4 entry condition: Ryan approves Week 3 report. Then GCP Console, OAuth staging E2E, optional SMTP.

Week 4 outcome (2026-08-25, corrected): Ryan's dedicated GCP OAuth client (889079898378-…) in Last1.id BWS project; GoTrue on VM 114 refreshed. Automated Google OAuth E2E PASS (authorize redirect uses Ryan's client_id). Full browser callback pending Ryan (no auth.last1.id DNS). LastVet synthetic OIDC E2E PASS; sovereign JWKS 200. Old last1app client (645620802422-…) active for hosted Supabase parallel run. STOP for Ryan Week 5 approval. Report: LAST1_ID_WEEK4_REPORT.md.

Week 4 correction: Initial agent pass wrongly inferred stale 911218996408-… from last1-admin BWS and stored all LAST1_ID_R940_* keys in lastvet project. Fixed by Ryan direct BWS update + migrate-last1id-bws-to-project.sh.

Task Status
Dedicated GCP OAuth client + BWS rotation Done
GoTrue Google provider on R940 Done
Google OAuth automated E2E (new client) Done
Google OAuth full browser callback Pending Ryan (hosts/tunnel; no DNS cutover)
LastVet partner OIDC E2E (synthetic) Done
Sovereign JWKS HTTP 200 + correct kid Done
SMTP / SendGrid identification Done (defer config)
Old last1app client parallel run Active (verified hosted Supabase 302)

Week 5 entry condition: Ryan approves Week 4 report + confirms GCP OAuth updates. Then full test matrix including JWKS 200 on public issuer.

Week 5 outcome (2026-08-25): Full test matrix on VM 114. Email/password PASS; Google OAuth browser PASS (Ryan manual); partner OIDC core + session cascade PASS; sovereign JWKS 200 / Railway 500; light load 10/10 PASS; Kong/MinIO failure modes PASS. Ryan review: partner id_token.sub ≠ auth.users.id (likely intended pairwise subject); Postgres iptables probe inconclusive (Docker egress). Report: LAST1_ID_WEEK5_REPORT.md.

W6A outcome (2026-08-25): Pre-cutover audit complete. Finding 1: LastVet uses id_token.sub as canonical key — no code fix. URLs: coordinated switch to https://auth.last1.id at W6B (sovereign + API + provider; iOS deferred post-soak). Finding 2: Postgres stop → GoTrue no crash, signup 500, auto-recovery. Report: LAST1_ID_WEEK6A_REPORT.md.

W6B-1 outcome (2026-08-25): Alerting verified. Postgres exporter up; new PostgresLast1idDatabaseDown (pg_up=0) live-fired; Last1idAuth5xxRateHigh + Caddy scrape up. Critical route same as N8nHostDown. Report: LAST1_ID_WEEK6B1_REPORT.md.

W6B outcome (2026-08-25): Coordinated cutover to https://auth.last1.id complete. Synthetic OIDC E2E ALL PASS. LastVet API + provider portal updated. Report: LAST1_ID_WEEK6B_REPORT.md.

W6C outcome (2026-08-26): 24h soak green (99/99 post-cutover probes). Ryan browser check PASS (health, OIDC, real Google OAuth). Week 6 closed at 24h. iOS issuer + my.last.vet Last1 sign-in remain deferred.

Week 7 outcome (2026-08-26): W7-1 audit PASS. W7-2 archival complete. W7-3 hosted Supabase deleted. W7-4 Railway service deleted. W7-5 legacy GCP OAuth deleted. W7-6 BWS cleanup (20 secrets deleted, 51→33 keys) + soak cron removed. Tier 2 CLOSED.

Week 8 outcome (2026-08-26): Migration plan finalized, muster items closed, sovereign runbook published, archive manifest refreshed. See LAST1_ID_MIGRATION_RUNBOOK.md.

Week 5 — Testing (44–68 h) — executed 2026-08-25

Flow Requirement
Email + password Signup, login, reset (SMTP live)
Google OAuth Both callback URLs (hosted + self-hosted) during parallel run
Partner OIDC LastVet provider portal + API with synthetic accounts
JWKS endpoint /.well-known/jwks.json returns HTTP 200 with correct RS256 keys (regression vs Railway prod 500)
PEM continuity Existing LastVet auth continues after issuer URL change (pre-cutover staging)
Profile photos Upload + read via MinIO

Gate: Ryan sign-off on test matrix before Week 6 DNS.

Week 6 — Cutover + 24–48h soak

Step Action
1 Deactivate Railway last1.id service schedules/traffic (keep service alive as fallback)
2 DNS: last1.id → Cloudflare Pages (marketing)
3 DNS: auth.last1.id → R940 tunnel (primary auth + OIDC issuer)
4 Deploy LastVet LAST1_ID_API_BASE=https://auth.last1.id (provider + API)
5 24–48h soak — Ryan founder call; monitor alerts; hosted Supabase + Railway remain restorable

Gate: No P1 auth failures; LastVet synthetic OIDC green for full soak window.

Weeks 7–8 — Teardown + playbook — COMPLETE 2026-08-26

Task Status Record
Pre-teardown dependency audit (W7-1) Done W7-1 audit
Archive Railway env + Supabase dump (W7-2) Done backups/last1id-railway-teardown-2026-08-26/
Delete hosted Supabase nztgswcpnwggymncrnyx (W7-3) Done supabase-deletion-record.json
Delete Railway last1.id service (W7-4) Done railway-deletion-record.json
GCP OAuth cleanup — legacy last1app client (W7-5) Done gcp-oauth-deletion-record.json
BWS cleanup + soak cron removal (W7-6) Done bws-deletion-record.json (20 deleted; 33 remain)
Sovereign ops runbook Done LAST1_ID_MIGRATION_RUNBOOK.md
Update consolidation plan + muster Done LAST1_INFRASTRUCTURE_CONSOLIDATION_PLAN.md

X-BWS — Bitwarden Secrets lane (Tier 2 complete)

Project: Last1.id · ID: cffb320a-5f61-4e1d-85df-b480012151ec
Live keys (2026-08-26): 33 — all prefixed LAST1_ID_R940_PROD_* (+ LAST1_ID_R940_POSTGRES_EXPORTER_PASSWORD)

Legacy unprefixed keys (SUPABASE_URL, OAUTH_JWT_*, Railway-era names) deleted W7-6. Full key map: LAST1_ID_MIGRATION_RUNBOOK.md.

Secret key Purpose
LAST1_ID_R940_PROD_DATABASE_URL Self-hosted Postgres (sovereign-db-last1id)
LAST1_ID_R940_PROD_GOTRUE_JWT_SECRET GoTrue session JWT signing (distinct from partner OIDC PEM)
LAST1_ID_R940_PROD_OAUTH_JWT_* Partner OIDC RS256 signing (preserved from Railway)
LAST1_ID_R940_PROD_GOOGLE_OAUTH_* Dedicated GCP client 889079898378-…
LAST1_ID_R940_PROD_SOVEREIGN_SERVICE_ROLE_KEY PostgREST service_role JWT
LAST1_ID_R940_PROD_SUPABASE_TLS_URL Public HTTPS API base (https://auth.last1.id)
LAST1_ID_R940_PROD_MINIO_* MinIO profile-photos bucket
Partner + gateway secrets {LASTVET,LAST1,ONSET,RADIOCHECK}_APP_SECRET, admin/bridge HMAC, Turnstile, Twilio

Hygiene follow-up: Prevent duplicate BWS entries if copy-railway-secrets-week3.sh is re-run (Week 3 created duplicate SUPABASE_URL key names).


Y10 — Recommended sequence (post-decision)

1. Ryan approves Week 1 kickoff date (STOP gate — this document)
2. Week 1: sovereign-db + auth-last1id VMs, tunnel, MinIO, BWS lane
3. Weeks 2–3: pg_dump restore drill → production import → MinIO photos
4. Weeks 3–4: GoTrue + app deploy; GCP OAuth callbacks BEFORE public cutover
5. Week 5: email, Google, LastVet OIDC synthetic tests; PEM/JWKS verify
6. Week 6: DNS split (last1.id → Pages, auth.last1.id → R940); 24–48h soak
7. Weeks 7–8: Railway + Supabase teardown; GCP callback cleanup; Tier 2 lessons
8. Post-stable: auth expansion (Apple, magic link, passkey) — one at a time

Deprecated: Mode 1 interim as end state; 7-day soak for Tier 2 (Ryan chose 24–48h).


STOP — Tier 2 closed

Item Status
Mode 2A execution COMPLETE (2026-08-26)
Sovereign production https://auth.last1.id
Legacy Railway + hosted Supabase Deleted (archive retained)
BWS Last1.id project 33 keys (cleaned)
Ops runbook LAST1_ID_MIGRATION_RUNBOOK.md

Next tier: Tier 3 (last1-admin) per LAST1_INFRASTRUCTURE_CONSOLIDATION_PLAN.md. Deferred items listed in Tier 2 closeout summary above.


Decisions log (formerly open questions)

# Question Resolution (2026-08-24)
1 Tier numbering Tier 2 = last1.id; admin → 3; RealOutcomes → 4; Onset → 5; bundle → 6
2 Migration mode Mode 2A (~240 h / 5–8 weeks)
3 Hostname Split: auth.last1.id auth; last1.id marketing (Pages)
4 Google OAuth GCP last1app, client "Last1.id"
5 Soak 24–48h (0 users; Railway + Supabase fallback)
6 Storage MinIO on R940 (not Supabase Storage API)

References

Document Path
Consolidation plan docs/architecture/LAST1_INFRASTRUCTURE_CONSOLIDATION_PLAN.md
Sovereign ops runbook docs/architecture/LAST1_ID_MIGRATION_RUNBOOK.md
Tier 1 runbook docs/architecture/N8N_MIGRATION_RUNBOOK.md
LastVet integration last.vet-api/docs/api/LAST1_ID_INTEGRATION.md
last1.id repo https://github.com/Last-1-Enterprises/last1.id
Linked providers doc last1.id/docs/LINKED_PROVIDERS.md
OIDC integration doc last1.id/docs/OAUTH_OIDC_INTEGRATION.md

Audit limitations (honesty box)

Source Access
last1.id source Shallow clone @ main
Railway service metadata GraphQL (domains, service id); memory/replicas/env names not via API
Supabase live DB Not accessed — schema from migrations only; row counts unverified
OAuth consoles Not accessed — Google/Apple client IDs unknown
BWS secret values Not read (constraint)
Off-repo products Documented via consolidation plan + internal-docs sync only

Tier 2 status: CLOSED 2026-08-26. Historical audit limitations above applied to pre-migration planning only.