Tier 4 Onset T-3 Gate B Report — Clinic Portal Last1 ID Auth Refactor¶
Status: PR ready — STOP for Ryan code review (no deploy)
Date: 2026-08-29
Prior: TIER_4_ONSET_GATE_A_REPORT.md
Repo: Last-1-Enterprises/clinic.theonset.app
Branch: t3-last1id-auth
Executive summary¶
Gate B replaces clinic portal GoTrue (password, Google, MFA) with Last1 ID OIDC via @last1id/sdk + encrypted httpOnly session cookie. Build passes. No deploy. iOS untouched.
RLS verdict (critical): Clinic portal runtime is RealOutcomes-simple after this refactor — identity from Last1.id JWT session; all practitioner data access uses service-role Supabase (bypasses auth.uid() RLS). Complexity moves to Gate D user-ID preservation (32 FKs → auth.users.id), not RLS rewrites.
RLS assessment (CRITICAL)¶
Database facts (hosted + R940 Gate B restore)¶
| Metric | Count |
|---|---|
| RLS policies (public schema) | 76 |
FK constraints → auth.users.id |
32 |
Migration SQL references to auth.uid() |
~85 across 11 migration files |
RLS policies do exist and do use auth.uid() — heavily, for patient-facing tables (connections, protocols, checkins, Fullscript isolation, etc.).
Does clinic portal use auth.uid() RLS at runtime?¶
Before Gate B (mixed):
| Path | Auth | DB client | RLS |
|---|---|---|---|
Route handlers (/api/clinic/*) |
GoTrue session | Service role | Bypassed |
Portal SSR pages (/(portal)/*) |
GoTrue session | Anon + JWT | auth.uid() applied |
| Middleware | GoTrue cookies | n/a | n/a |
After Gate B (unified service-role for practitioners):
| Path | Auth | DB client | RLS |
|---|---|---|---|
| Login / callback | Last1.id PKCE | n/a | n/a |
| Middleware | clinic_last1_session cookie |
n/a | n/a |
| Route handlers | Last1 session → requireClinicPractitioner() |
Service role | Bypassed |
| Portal SSR pages | Last1 session → getClinicPortalContext() |
Service role | Bypassed |
/api/protocols/v2/[patientId]/latest |
Unchanged — GoTrue Bearer (iOS patients) | Anon + JWT | auth.uid() (patient path only) |
Answer for Ryan: Retiring local GoTrue for clinic portal does not require rewriting the 76 RLS policies. The portal never depended on RLS for authorization logic at the API layer; SSR pages are migrated to the same service-role pattern as APIs. auth.uid() policies remain for iOS app + edge functions + the one dual-purpose protocol API.
Who still depends on auth.uid() RLS?¶
- iOS app (hosted GoTrue until App Store sovereign) — unchanged in T-3
- Supabase Edge Functions (patient/practitioner RPC paths)
/api/protocols/v2/[patientId]/latestpatient self-access branch (GoTrue JWT)
Gate D: user-ID preservation (harder than RLS)¶
RealOutcomes lesson: id_token.sub must equal existing auth.users.id or FK rows break.
| Operator | Gate D action | |
|---|---|---|
| Ryan | curryrg@gmail.com |
Remap / link Last1 identity → existing auth.users.id |
| Test | hello@unicornryda.com |
Same |
Until Gate D: Last1 sign-in succeeds but practitioners.user_id lookup fails for real accounts.
T-3 complexity: Runtime RLS = RealOutcomes-simple. Gate D FK remap = meaningful work (same class as RealOutcomes Gate D).
Confirmations¶
| Item | Result |
|---|---|
| Clinic port | 3000 (Dockerfile EXPOSE 3000, .env.example, render script PORT=3000). RealOutcomes uses 5000. |
| iOS untouched | Yes. Zero changes in last.vet-ios / Onset iOS repo. iOS stays on hosted GoTrue (emxrmjysvxeboavhatry). Shared onset OAuth client preserved for iOS Last1 linking. |
trust_events:write |
Zero hits in clinic codebase. Correctly omitted at Gate A. |
| Secret rotation | Not rotated (Gate A). Shared with iOS. |
| Deploy | None. PR only. |
Auth architecture (Gate B)¶
Browser → Last1.id PKCE → /auth/callback (client)
→ POST /api/auth/last1/callback (token exchange + JWKS)
→ completeClinicAuth (service role)
→ Set clinic_last1_session cookie (AES-256-GCM)
Middleware → cookie presence check
Portal/API → getClinicPortalContext / requireClinicPractitioner → service role
Removed: password login, Google OAuth, Supabase MFA routes, GoTrue app_metadata tagging.
Deferred (Gate D): new owner signup (/api/clinic/register still GoTrue), team invite email confirm, /api/clinic/onboard owner-create path (403).
Files changed¶
Added¶
| File | Purpose |
|---|---|
src/lib/last1-config.ts |
Env helpers |
src/lib/last1Auth.ts |
Browser PKCE (@last1id/sdk) |
src/lib/last1IdOAuth.ts |
Server token exchange + JWKS |
src/lib/clinic-session.ts |
Encrypted session cookie |
src/lib/clinic-session-constants.ts |
Edge-safe cookie name |
src/app/api/auth/last1/callback/route.ts |
Token exchange handler |
src/app/auth/callback/page.tsx |
OAuth redirect landing |
Modified (high signal)¶
| File | Change |
|---|---|
src/middleware.ts |
Last1 session cookie gate |
src/lib/clinic-api.ts |
requireClinicPractitioner, getClinicPortalContext |
src/app/login/page.tsx |
Last1 ID sign-in only |
src/app/(portal)/layout.tsx + 12 SSR pages |
Last1 session + service role |
src/app/api/clinic/*, stripe, billing |
Last1 session |
package.json |
@last1id/sdk, jose |
.env.example, Dockerfile |
Last1 OAuth + session secret keys |
Removed¶
| File |
|---|
src/app/api/auth/login/route.ts |
src/app/api/auth/mfa/* |
src/app/auth/callback/route.ts (GoTrue exchange) |
src/components/google-sign-in-button.tsx |
Infra (lastvet-gates)¶
| File | Purpose |
|---|---|
scripts/onset/render-onset-env-app.sh |
BWS → /opt/onset/.env (port 3000, NEXT_PUBLIC_* Last1 keys) |
BWS keys still needed before Gate C deploy¶
| Key | Notes |
|---|---|
ONSET_APP_PROD_CLINIC_SESSION_SECRET |
32+ byte random; encrypts clinic_last1_session |
| Gate A keys | Already in onset BWS (LAST1_OAUTH_*, LAST1_ID_ISSUER) |
Build / test¶
| Check | Result |
|---|---|
npm run build |
PASS |
| Deploy | Not run (Gate B scope) |
| Live OIDC soak | Gate C |
Known gaps (Ryan review)¶
/api/clinic/register— still creates GoTrue users; new signup UI stubbed/auth/confirm— legacy GoTrue links redirect to login/api/protocols/v2/*— patient branch still GoTrue (intentional for iOS)patients/new/page.tsx— client component still calls anon Supabase for profile bootstrap (uses API routes for writes)- Gate D — identity remap before production login works for real practitioners
STOP — Ryan Gate B code review¶
| Deliverable | Status |
|---|---|
Clinic @last1id/sdk refactor |
Done (PR) |
| RLS assessment | Above — RealOutcomes-simple runtime; Gate D FK remap |
| Port 3000 | Confirmed |
| iOS untouched | Confirmed |
trust_events:write |
Not present |
| Deploy | None |
Next after approval: Gate C deploy to VM 118 + LAN soak. Not before Ryan sign-off.