ROADMAP¶
C0 2026-09-13: Hosted refs below (
tcthzakppndkykatoefx,nztgswcpnwggymncrnyx) are HISTORICAL. Those projects are deleted. Do not treat this snapshot as a live deploy target.
Snapshot of outstanding work across the last1.id ecosystem. Maintained
as a checklist so the next operator (human or agent) can pick up
without re-discovering context.
Last updated: 2026-05-23 — Thread 1 + Thread 2 shipped to prod across all 6 repos; LastVet PRs #12/#15/#16 merged + auto-migrate; RadioCheck PRs #1/#5/#6/#7/#8 merged + Edge Functions deployed; webhook secrets provisioned end-to-end; broken check-in-proof-outbox cron removed (RadioCheck #8); computeRecommendedAction helper deduped (last1.id #67).
The Last1 ID ecosystem spans six repos:
| Repo | Purpose |
|---|---|
last1.id |
Core identity platform — OAuth/OIDC issuer, partner webhook drainer, end-user dashboard |
last1-admin |
Operator console (server-side BFF + React UI) for managing partners, identities, app clients, webhook config |
last.vet-api |
LastVet backend — partner OAuth consumer + webhook receiver |
last.vet-provider |
LastVet provider portal — Next.js |
last.vet-ios |
LastVet veteran iOS app — native Swift |
RadioCheck |
RadioCheck partner — Supabase Edge Functions + iOS RN |
Items in this roadmap reference PRs / commits in those repos. When you complete an item, tick its checkbox and (if non-obvious) leave a one-line note about what you actually did.
1. Open PRs awaiting merge¶
All previously-tracked PRs are merged and deployed as of 2026-05-23.
The dependency arrow → notes are retained as historical context for
future stacked-PR chains.
1a. React 19 + Vite 8 chain (last1.id) ✅¶
- [x] last1.id #50 — React 19 migration
- [x] last1.id #51 — Vite 8 +
@vitejs/plugin-react@6
1b. Connected Apps end-user surface (last1.id) ✅¶
- [x] last1.id #52 — Enrich Connected Apps view (
granted_at,active_sessions,app_status, scope descriptions, error UX, richer confirm)
1c. Admin forced unlink chain (last1.id + last1-admin) ✅¶
- [x] last1.id #46 — backend
GET/POST /api/admin/identities/:id/app-links[*/revoke] - [x] last1-admin #2 — frontend Connected Apps dialog + Force Unlink confirm (Railway redeploy bumped to Node 22 alongside the merge so Supabase Realtime client wouldn't crash at boot)
1d. LastVet partner stack (last.vet-api + last.vet-provider + last.vet-ios) ✅¶
API:
- [x] last.vet-api #12 — provider-side proxy to last1.id partner reads
- [x] last.vet-api #15 (re-rooted from closed #13) — partner-side webhook handler (inbound revoke from last1.id)
- [x] last.vet-api #16 (re-rooted from closed #14) — POST /api/auth/last1/(veteran/)unlink explicit user-driven disconnect
- [x] last.vet-api — scripts/railway-start.sh now runs npm run migrate before boot so migration drift can't ship; migrations 023 + 024 verified applied via Railway logs
Provider portal: - [x] last.vet-provider #4 — Disconnect from Last1 ID button
iOS: - [x] last.vet-ios #2 — enable Link Last1 ID (kill-switch flipped on) - [x] last.vet-ios #4 (re-rooted from closed #3) — Disconnect from Last1 ID button
TestFlight push of the iOS app is still a manual user action — the PRs are on main but the binary has not yet been uploaded.
1e. RadioCheck partner stack ✅¶
- [x] RadioCheck #1 — strip PII from check-in trust events + schedule proof outbox drain
- [x] RadioCheck #5 — untrack
.env+ add secrets rotation runbook - [x] RadioCheck #6 — post-link "Connected to Last1" screen
- [x] RadioCheck #7 — inbound revoke webhook (closes the 1-hour gap)
- [x] RadioCheck #8 (follow-up) — unschedule the duplicate
check-in-proof-outboxcron that PR #1 introduced (was erroring every minute against unsetapp.settings.*parameters; the oldercheck-in-proof-events-cronalready drains the same outbox via the working Vault pattern)
Both new Supabase Edge Functions (check-in-proof-events updated to use mapping.ts, new revoke-from-last1) and both migrations (20260521000000_schedule_check_in_proof_outbox and 20260523000000_last1_revoke_webhook_deliveries) deployed to project tcthzakppndkykatoefx.
1f. SDK Next.js companion (last1.id) ✅¶
- [x] last1.id #45 —
@last1id/sdk-nextjs@0.1.0package merged and published (2026-06-02).
2. Hard-blocked items¶
2a. npm publishing chain ✅¶
Published 2026-06-02 via granular Automation token + NPM_TOKEN on GitHub:
- [x]
@last1id/sdk@0.3.0 - [x]
@last1id/login-button@0.1.3(sdk peer^0.3.0) - [x]
@last1id/sdk-react-native@0.1.0 - [x]
@last1id/sdk-nextjs@0.1.0 - [x]
gh secret set NPM_TOKEN -R Last-1-Enterprises/last1.id - [x] Verified
npm view @last1id/sdk@0.3.0resolves
Unblocked the inline copy → SDK import swaps in § 4a.
2b. Secrets / environment provisioning¶
User-only, no PR can replace these:
- [ ] Complete rotations in
last1.id/docs/SECRET_ROTATION_RUNBOOK.mditems #1-5; tick off each as completed in that file - [ ] Create Sentry project for
last1-id; setSENTRY_DSN,SENTRY_ENVIRONMENT,SENTRY_RELEASEon Railway - [x] Migration
20260523000000_partner_webhooks.sqlapplied to prod - [x] Migration
20260523010000_app_clients_home_url.sqlapplied to prod (Thread 2a) - [x] LastVet
app_clientswebhook config:webhook_url=https://api.last.vet/api/webhooks/last1,webhook_secret_b64provisioned,webhook_enabled=true;LAST1_WEBHOOK_SECRET+LAST1_TOKEN_ENCRYPTION_KEYset on Railway (matching secret fingerprint verified both sides) - [x] RadioCheck
LAST1_INBOUND_WEBHOOK_SECRETset viasupabase secrets set;revoke-from-last1Edge Function deployed - [x] RadioCheck
app_clientswebhook config (last1.id-side):webhook_url=https://tcthzakppndkykatoefx.supabase.co/functions/v1/revoke-from-last1,webhook_secret_b64provisioned,webhook_enabled=true - [x] LastVet migration 023 (provider Last1 token storage) applied on Railway via the new
npm run migratehook inscripts/railway-start.sh; migration 024 (last1_webhook_deliveries) also picked up by the same hook
3. End-to-end smoke tests (post-deploy)¶
These require all of the relevant PRs from § 1 to be merged AND the relevant secrets from § 2b to be configured.
Click-paths, curl commands, and SQL verifications for every item below live in
docs/E2E_SMOKE.md. Use that file as the working surface during the actual smoke run; this section is the index. The verification half of every revoke check is automated byscripts/smoke-verify-revoke.mjs.
3a. RadioCheck connected screen & cross-app activity¶
- [ ] Manually smoke-test the connected screen in TestFlight (fresh link, already-linked, 401 path, pull-to-refresh)
- [ ] Trigger a RadioCheck check-in; verify the trust event arrives in Care Connections and Activity within 5 min
- [ ] Trigger a responder-added event (add a check-in responder); verify
radio_check_responder_addedappears in Activity with role metadata only (no message content) - [ ] On Dashboard, verify Trust summary (
ReputationSummary) shows non-zero dimensions when partner events exist - [ ] In Settings, toggle one trust dimension public; verify
/id/:usernameshows only opted-in dimension bars - [ ] Last1.app linked user: enroll in a program or record an outcome; verify
last1.*trust event in Activity after outbox drain (seeE2E_SMOKE.md§6)
3b. Cross-app revoke from last1.id¶
- [ ] Link a throwaway veteran account to RadioCheck via the partner OAuth flow
- [ ] Revoke from
last1.id→ Connected Apps page - [ ] Verify
users.last1_identity_id = NULLon the RadioCheck Supabase project - [ ] Verify a
last1_webhook_deliveriesrow exists withaction = 'identity_unlinked'
3c. Admin forced unlink¶
- [ ] Link a throwaway account to LastVet and RadioCheck
- [ ] Use the last1-admin Connected Apps → Force Unlink dialog on the test identity
- [ ] Verify partner-side state is cleared on both
- [ ] Verify last1.id
admin_audit_logsshows the entry withaction = 'identity.app_link.force_revoke'and the reason string the operator typed
3d. End-user disconnect (this session, option I + D)¶
- [ ] On a linked account, visit
last1.id /dashboard/apps— verify permissions, active sessions, and last-used render correctly (PR #52) - [ ] Click Revoke → confirm dialog lists permission count + session count → confirm → row disappears
- [ ] Verify partner-side state clears (RadioCheck
users.last1_identity_id = NULL; LastVetproviders.last1_identity_id = NULL) - [ ] On LastVet provider portal, visit Profile → Linked Accounts; verify Disconnect button surfaces and works (PR #4 + #14)
- [ ] On LastVet iOS app, visit Profile → Linked Accounts; verify Disconnect button surfaces and works (PR #3 + #14)
3e. Verification checklist (parked from earlier session)¶
- [ ] DELETE flow on throwaway account
- [ ] Boot validator catches missing required env
- [ ] PII scrubber returns 400 on PII-containing trust event payloads
- [ ]
account-existsrate-limit fires after threshold - [ ]
X-Request-Idcorrelates through the whole request lifecycle - [ ] Sentry actually captures a thrown error
4. Quality / cleanup follow-ups (non-blocking)¶
4a. Swap inline SDK copies for npm imports ✅¶
Completed 2026-06-02 after @last1id/sdk@0.3.0 landed on npm:
- [x] LastVet
last.vet-api— deleted inlinelast1ProfileClient.ts+last1WebhookSigning.ts; routes import from@last1id/sdk@^0.3.0(187/187 tests pass) - [x] RadioCheck
supabase/functions/revoke-from-last1— deleted inlineverifyWebhookSignature.ts; Deno import map usesnpm:@last1id/sdk@0.3.0(24/24 tests pass)
4b. React 19 / Vite 8 deferred cleanups ✅¶
Code-level follow-ups confirmed done as of 2026-05-23 (verified by rg over client/src):
- [x] All shadcn UI components migrated off
React.forwardRef(zero matches remain) - [x]
React.ElementRefswapped toReact.ComponentRef(zero matches remain) - [x]
useContext→use()decision: no callers remain (zero matches) - [x] Vite 8 chunk-size warning addressed via
manualChunksfunction form invite.config.ts(query-vendor / supabase-vendor split;chunkSizeWarningLimit: 400retained)
4c. Connected Apps last-used backfill ✅¶
- [x]
server/routes/oauth.tsuserinfo handler (lines 989-1007) now updates bothconsent_grants.last_used_atandidentity_app_links.last_used_atin aPromise.allfire-and-forget. The fallback path in/api/identity/app-linksremains for historic rows; backfill of pre-existing rows is optional and skipped (the cost of a stale "Last used —" cell for legacy data is acceptable; new userinfo calls heal the row).
4d. Observability migration — partner-facing surface done, lib/route catch blocks remain¶
- [x]
server/routes/partnerReads.ts(8 calls),server/routes/credentials.ts(1 call),server/lib/oauthRevocation.ts(3 calls) migrated tologger.errorwith structured fields (route, app_id, identity_id, error_code). Fixed an incidental swallowed audit-log error bug inoauthRevocation.tswhile there (the audit-log insert returned{ error }rather than throwing — oldtry/catchonly handled throws). - [ ] Remaining ~110
console.*calls acrossserver/routes/{identity,admin,oauth,appClients,orgAdmin,trustEvents}.tsandserver/lib/{userExport,userAppRevoke,userActivity,maintenance,envValidation}.ts. These are mostly bareconsole.error(err)in catch blocks; converting them is mechanical but each conversion should include a structured event name + route/identity context (seepartner_read.*for the pattern).
4e. RadioCheck cron health ✅ (partial)¶
- [x] Broken
check-in-proof-outboxcron from RadioCheck PR #1 unscheduled (RadioCheck #8). It was duplicating the workingcheck-in-proof-events-cronbut usingcurrent_setting('app.settings.supabase_url')/app.settings.service_role_keyparameters that aren't configured in this project — every minute it errored at the SQL level. Working cron continues to drain the outbox every 5 min. - [ ] Investigate whether
sms-checkin-cronandorg-sms-cron(daily at 14:00 UTC) are actually firing successfully. The earlier roadmap note ("function reads secrets at module init") doesn't match the current code, which reads inside the handler. Worth a single SQL spot-check ofcron.job_run_detailsafter the next 14:00 UTC tick to confirm.
4f. Pre-existing typecheck errors¶
- [x] Fix the 4 pre-existing typecheck errors in
server/storage.ts+server/validation-engine/*— resolved incidentally by recent PRs. Verifiedtscclean onmain2026-05-22.
5. Future work (not yet PRs)¶
Things discussed but not implemented. No deadline; capture so they don't get lost.
- K. End-to-end full-chain smoke — single throwaway veteran account that exercises every link, revoke, and admin path in §3 in a single test session. Useful as a release-gate. Partially automated as of 2026-05-23: production-readiness curl probes verified all 6 backends + every new endpoint mounts and 401s when unauthenticated;
/.well-known/openid-configurationadvertisesprompt_values_supported:["none"]. The click-through portion (TestFlight, browser OAuth) still requires a human. - L.
identity_app_links.last_used_atbump in OAuth path — Done. See § 4c above. - M. React-19-idiomatic cleanups — see § 4b. Codemod-friendly.
- N. "Disconnect from all apps" button on
/dashboard/apps— fan-out revoke to every linked partner in one click, with confirm. Mirror of admin's per-app force-unlink but iterating over the user's full list. Done (Thread 1a —feat/revoke-all-apps). AddsPOST /api/identity/me/revoke-all-apps, therevokeAppLinkForIdentityhelper atserver/lib/userAppRevoke.ts(also used by future account-delete), and a "Disconnect from all apps" button on the Connected Apps page. Emitsapp_link.revokedper linked partner withsource: "user_initiated_revoke_all". - Sovereignty-grade portable data export (Thread 1c) —
GET /api/identity/me/exportreturns a JWS-signed (Ed25519, via the platform's existing system issuer key) JSON bundle containing identity + credentials + issuers + reputation snapshots + full consent grant history + full app-link history + linked auth providers + raw trust events. Anyone can verify the bundle by resolving thekidvia did:web — no shared secret needed. Done (PRfeat/data-export). The legacy/credentials/exportHMAC endpoint stays in place for the/credentials/importround-trip flow. - Cross-app "Sign in everywhere" / SSO landing — single Last1 ID session lights up partner-app sessions automatically. Shipped as Thread 2 (4 PRs):
- 2a. "Open this app" launcher —
home_urlcolumn onapp_clients+ Open button on/dashboard/apps. Done (PRfeat/cross-app-launcher). LastVet + RadioCheck seeded; other partners null until configured. Hidden for non-active apps. - 2b. Session-status endpoint —
GET /api/identity/me/session-status?app_id=<slug>returns{ linked, consent: {granted, scopes, granted_at, last_used_at}, active_sessions, recommended_action }.recommended_action ∈ {link, reauth, refresh, open}drives the next-step decision for both partners (Bearer-auth) and the dashboard itself. Done (PRfeat/session-status). - 2c. OIDC silent auth —
GET /oauth/authorize?prompt=none&id_token_hint=…returns 302 withcodeor OIDC-standarderror=login_required/error=consent_required. Usesid_token_hint(OIDC §3.1.2.1) for session detection instead of a cookie, withaud === client_idpinning to prevent cross-partner id_token replay. Falls through to the SPA for non-silent requests. Done (PRfeat/silent-auth). - 2d. Active-session indicators on Connected Apps cards — surfaces the
recommended_action(computed inline on/api/identity/app-links) as a per-card status badge: amber "Sign-in needed" forrefresh, red "Access revoked" forreauth, nothing foropen(the existing active-sessions badge already covers it). Done (PRfeat/active-session-indicators). - Veteran-side full audit log — show the user the same
admin_audit_logsview operators see, scoped to their own identity. Reuses the admin shape from PR #46. Done (Thread 1d —feat/user-activity-log). Now exposed at/trust-ledger(sidebar item "Activity"). AddsGET /api/identity/me/activityaggregatingoauth_token_audit_log,consent_audit_log,trust_events, andadmin_audit_logsinto a unified, PII-redacted, paginated feed; UI is grouped-by-day timeline with per-category icons and per-event disclosure of safe metadata. - Account deletion partner webhook fan-out (Thread 1b) —
DELETE /api/identity/menow emits oneidentity.deletedwebhook per previously-linked partner (source: "user_initiated_account_delete"). Endpoint and confirmation UI inclient/src/pages/settings.tsxalready existed; this only added the cross-app notification gap. Done (PRfeat/account-delete-webhooks). AddsIdentityDeletedPayloadtoserver/lib/webhookEmission.tsand a 11-case integration test for the route's snapshot → cascade → fan-out orchestration. @last1id/sdkfor Swift — analogous tosdk-react-native+sdk-nextjs, but for native iOS. Would absorb the inline copy inlast.vet-ios/LastVet/Core/Networking/LastVetAPIClient.swift(thelinkLast1ID/unlinkLast1IDblocks).- Dependabot major-bump migrations:
react-19-migration— covered by #50 (was the partial-bump #33 that got closed). Done.vite-8-migration— covered by #51 (was the partial-bump #35 that got closed). Done.- Future: keep watching for paired-package partial bumps where Dependabot proposes one but not its dependency.
computeRecommendedActiondedup —getSessionStatusForApp(Thread 2b) and the batched/api/identity/app-linkshandler (Thread 2d) both computed the link/reauth/refresh/open state machine. Extracted into a pure helper inserver/lib/sessionStatus.ts. Done (PR #67).- Auto-run LastVet migrations on Railway boot —
scripts/railway-start.shnow invokesnode scripts/migrate.jsbefore booting the server. Migrations are idempotent (schema_migrations PK on filename) and race-safe between replicas, and the deploy now fails-fast when a migration breaks rather than booting against a stale schema. Done (commitc852471).
6. Working agreements¶
How this roadmap stays useful:
- Update when you ship. When you merge a PR or check off an item, edit this file in the same commit (or a fast-follow). Stale roadmap is worse than no roadmap.
- Reference PRs by number. Easier to follow chains across repos.
- Prefer concrete user actions over verbs. "Run
supabase functions deploy revoke-from-last1" beats "deploy RadioCheck PR". The next operator may not have your context. - Annotate blockers. If you park something, say what unblocks it (token, deploy, support ticket). § 2a is the model.