Skip to content

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)
  • [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-outbox cron that PR #1 introduced (was erroring every minute against unset app.settings.* parameters; the older check-in-proof-events-cron already 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.0 package 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.0 resolves

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.md items #1-5; tick off each as completed in that file
  • [ ] Create Sentry project for last1-id; set SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE on Railway
  • [x] Migration 20260523000000_partner_webhooks.sql applied to prod
  • [x] Migration 20260523010000_app_clients_home_url.sql applied to prod (Thread 2a)
  • [x] LastVet app_clients webhook config: webhook_url=https://api.last.vet/api/webhooks/last1, webhook_secret_b64 provisioned, webhook_enabled=true; LAST1_WEBHOOK_SECRET + LAST1_TOKEN_ENCRYPTION_KEY set on Railway (matching secret fingerprint verified both sides)
  • [x] RadioCheck LAST1_INBOUND_WEBHOOK_SECRET set via supabase secrets set; revoke-from-last1 Edge Function deployed
  • [x] RadioCheck app_clients webhook config (last1.id-side): webhook_url=https://tcthzakppndkykatoefx.supabase.co/functions/v1/revoke-from-last1, webhook_secret_b64 provisioned, webhook_enabled=true
  • [x] LastVet migration 023 (provider Last1 token storage) applied on Railway via the new npm run migrate hook in scripts/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 by scripts/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_added appears 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/:username shows 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 (see E2E_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 = NULL on the RadioCheck Supabase project
  • [ ] Verify a last1_webhook_deliveries row exists with action = 'identity_unlinked'
  • [ ] 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_logs shows the entry with action = '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; LastVet providers.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-exists rate-limit fires after threshold
  • [ ] X-Request-Id correlates 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 inline last1ProfileClient.ts + last1WebhookSigning.ts; routes import from @last1id/sdk@^0.3.0 (187/187 tests pass)
  • [x] RadioCheck supabase/functions/revoke-from-last1 — deleted inline verifyWebhookSignature.ts; Deno import map uses npm:@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.ElementRef swapped to React.ComponentRef (zero matches remain)
  • [x] useContext → use() decision: no callers remain (zero matches)
  • [x] Vite 8 chunk-size warning addressed via manualChunks function form in vite.config.ts (query-vendor / supabase-vendor split; chunkSizeWarningLimit: 400 retained)

4c. Connected Apps last-used backfill ✅

  • [x] server/routes/oauth.ts userinfo handler (lines 989-1007) now updates both consent_grants.last_used_at and identity_app_links.last_used_at in a Promise.all fire-and-forget. The fallback path in /api/identity/app-links remains 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 to logger.error with structured fields (route, app_id, identity_id, error_code). Fixed an incidental swallowed audit-log error bug in oauthRevocation.ts while there (the audit-log insert returned { error } rather than throwing — old try/catch only handled throws).
  • [ ] Remaining ~110 console.* calls across server/routes/{identity,admin,oauth,appClients,orgAdmin,trustEvents}.ts and server/lib/{userExport,userAppRevoke,userActivity,maintenance,envValidation}.ts. These are mostly bare console.error(err) in catch blocks; converting them is mechanical but each conversion should include a structured event name + route/identity context (see partner_read.* for the pattern).

4e. RadioCheck cron health ✅ (partial)

  • [x] Broken check-in-proof-outbox cron from RadioCheck PR #1 unscheduled (RadioCheck #8). It was duplicating the working check-in-proof-events-cron but using current_setting('app.settings.supabase_url') / app.settings.service_role_key parameters 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-cron and org-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 of cron.job_run_details after 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. Verified tsc clean on main 2026-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-configuration advertises prompt_values_supported:["none"]. The click-through portion (TestFlight, browser OAuth) still requires a human.
  • L. identity_app_links.last_used_at bump 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). Adds POST /api/identity/me/revoke-all-apps, the revokeAppLinkForIdentity helper at server/lib/userAppRevoke.ts (also used by future account-delete), and a "Disconnect from all apps" button on the Connected Apps page. Emits app_link.revoked per linked partner with source: "user_initiated_revoke_all".
  • Sovereignty-grade portable data export (Thread 1c) — GET /api/identity/me/export returns 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 the kid via did:web — no shared secret needed. Done (PR feat/data-export). The legacy /credentials/export HMAC endpoint stays in place for the /credentials/import round-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_url column on app_clients + Open button on /dashboard/apps. Done (PR feat/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 (PR feat/session-status).
  • 2c. OIDC silent auth — GET /oauth/authorize?prompt=none&id_token_hint=… returns 302 with code or OIDC-standard error=login_required / error=consent_required. Uses id_token_hint (OIDC §3.1.2.1) for session detection instead of a cookie, with aud === client_id pinning to prevent cross-partner id_token replay. Falls through to the SPA for non-silent requests. Done (PR feat/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" for refresh, red "Access revoked" for reauth, nothing for open (the existing active-sessions badge already covers it). Done (PR feat/active-session-indicators).
  • Veteran-side full audit log — show the user the same admin_audit_logs view 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"). Adds GET /api/identity/me/activity aggregating oauth_token_audit_log, consent_audit_log, trust_events, and admin_audit_logs into 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/me now emits one identity.deleted webhook per previously-linked partner (source: "user_initiated_account_delete"). Endpoint and confirmation UI in client/src/pages/settings.tsx already existed; this only added the cross-app notification gap. Done (PR feat/account-delete-webhooks). Adds IdentityDeletedPayload to server/lib/webhookEmission.ts and a 11-case integration test for the route's snapshot → cascade → fan-out orchestration.
  • @last1id/sdk for Swift — analogous to sdk-react-native + sdk-nextjs, but for native iOS. Would absorb the inline copy in last.vet-ios/LastVet/Core/Networking/LastVetAPIClient.swift (the linkLast1ID / unlinkLast1ID blocks).
  • 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.
  • computeRecommendedAction dedup — getSessionStatusForApp (Thread 2b) and the batched /api/identity/app-links handler (Thread 2d) both computed the link/reauth/refresh/open state machine. Extracted into a pure helper in server/lib/sessionStatus.ts. Done (PR #67).
  • Auto-run LastVet migrations on Railway boot — scripts/railway-start.sh now invokes node scripts/migrate.js before 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 (commit c852471).

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.