1D Admin UI — Backend → UI Surface Inventory
Status: audit output, not a plan. Produced 2026-05-07 immediately after foundation 1C closed. Author: Claude (Opus 4.7) audit pass. Scope: every shipped foundation backend (1A–1C) mapped to the UI surface it needs in 1D, plus the 1D primitives that don't exist yet, plus the OpenAPI / api-client gaps that block UI work.
This document does not propose new features, redesign existing pages, sequence work, or estimate effort. It catalogs what exists in the backend, what UI it needs, what UI is already built, and where the gaps are. The follow-on 1D execution plan consumes this inventory; producing that plan is a separate exercise.
How to read this doc
- The four 1D sub-phases (1D.1 Console, 1D.2 Clinic admin, 1D.3 Portal, 1D.5 Cross-org Account) get one section each. Each surface entry lists its backend endpoints, required permission, OpenAPI status, api-client status, and UI status (built / stub / not started).
- 1D.4 (Shared UI Patterns) is its own section — primitives the per-app surfaces consume.
- A "Gaps" section flags every place where a UI surface is blocked by missing OpenAPI or missing api-client wrapper.
- A "System-only — no UI needed" section enumerates backend functionality that exists for cron/internal purposes; future engineers shouldn't re-ask whether it needs UI.
- A "Decisions / ambiguities" section flags everything that needs human input before 1D starts.
Authoritative sources cited throughout
apps/docs/implementation-plan/foundation.md— sub-phase status and 1D.1–1D.5 scope (line numbers cited inline)services/api/internal/core/server/routes.go— every mounted routeservices/api/internal/core/domain/{name}/handler.go— endpoint behavior + permission gatesapps/docs/openapi.yaml— current spec (incomplete; see Gaps)packages/api-client/src/client.ts— existing client wrappers (hand-written; see Gaps)apps/clinic/app/,apps/portal/app/,apps/console/app/— existing UIapps/docs/architecture/{glossary,data-model,patterns,decisions}.md— taxonomy, schema, conventions
When a feature spec under apps/docs/features/ describes UI for a backend slice that doesn't exist yet, that surface is in the "Out of 1D scope" section. Per CLAUDE.md → Foundation Discipline, feature specs predate the holistic schema audit and lose to architecture docs on schema details.
Top-line counts
Counts below are pre-decision snapshots from the 2026-05-07 audit. Effective post-decision counts (after the 12 decisions resolved 2026-05-07) are in the Appendix — surface counts by app; the deltas come from D-1 / D-3 / D-6 / D-7 (six rows removed) and D-12 (31 F-tier sidebar entries pruned).
| Item | Count |
|---|---|
| Distinct UI surfaces catalogued (across 1D.1 + 1D.2 + 1D.3 + 1D.5) | 121 |
| Surfaces already built (full or partial) | 22 |
| Surfaces with nav stub but no implementation | 41 |
| Surfaces with no nav entry yet | 58 |
| Backend endpoints (foundation 1A–1C) | 105 across 24 domain handlers |
| Endpoints with OpenAPI entry | ~53 (~50% coverage) |
| Endpoints with api-client wrapper | ~38 (~36% coverage) |
| Distinct permission codes the UI needs to honor | ~30 |
| Shared UI primitives needed (1D.4) but not yet built | 4 (toast, RequirePermission, server-validation renderer, empty/loading/error states) |
| Backend tables introduced in 1A–1C | ~50 |
| Backend domain handlers with zero OpenAPI coverage | 12 (locations, impersonation, webhooks, integrations, integration-services catalog, platform-providers, ai-models, break-glass, audit, invitations, consents, legal-documents) |
| Decisions resolved 2026-05-07 | 12 of 12 (D-1 through D-12) — see Decisions |
| Surfaces removed by decisions | 6 (C19, C21, C24, C25, C33, C34) |
| Surfaces moved to 1E | 1 (C30 system health) |
| Net 1D-scoped surfaces post-decision | 73 (was 79) |
The OpenAPI gap is the single biggest blocker — see § Gaps to flag.
1D.4 — Shared UI Patterns (cross-cutting)
packages/ui/ already ships the building blocks for tables, sidebars, dialogs, etc. The 1D.4 work is the conventions every per-app surface depends on. From foundation.md:1300–1316:
| Primitive | Status | Where it lives / will live | Consumer surfaces (every app) |
|---|---|---|---|
DataTable (TanStack-backed; server-driven sort / filter / pagination + detail Sheet) | Built | packages/ui/src/components/data-table.tsx | Every list page |
MultiSelectFilter, AsyncMultiSelectFilter, DateRangeFilter | Built | packages/ui/src/components/{multi-select-filter,async-multi-select-filter,date-range-filter}.tsx | Audit, lists, search |
| App shell + brand theme (sidebar, OKLCH tokens, Poppins, light/dark) | Built | packages/ui/src/components/sidebar.tsx; per-app components/app-sidebar.tsx | Every dashboard |
| Listing-page pattern (fill mode, sticky toolbar, edge-to-edge tables) | Built | Pattern in apps/console/app/(dashboard)/audit-logs/page.tsx | Every list page |
| Branded 404 pages with i18n | Built | Each app's not-found.tsx | All apps |
<RequirePermission /> helper | Not built | Belongs in packages/ui/src/patterns/ | Every gated control across all 3 apps |
| Toast / notification system | Not built | Belongs in packages/ui/src/components/ | Every mutation feedback (publish, save, delete, copy, etc.) |
| Server-validation rendering (422 field errors → form field error states) | Not built | Belongs in packages/ui/src/patterns/ | Every form (org settings, billing, consent, share-link mint, etc.) |
| Empty / loading / error states standardised | Not built | Belongs in packages/ui/src/patterns/ | Every list and detail page |
AsyncMultiSelectFilter for entity pickers (P-required: GIN trigram + unaccent) | Built | Already used by audit-logs | Audit, members, patients, specialists, segments |
Existing packages/ui primitives a 1D engineer can reuse without thinking: button, input, form-field, textarea, input-group, checkbox, switch, select, card, dialog, dropdown-menu, popover, tabs, collapsible, badge, skeleton, tooltip, breadcrumb, separator, table, calendar, avatar. Patterns: field, section-card-header, stat-card, status-pill, user-avatar. Hooks: use-mobile, use-server-synced-state.
Resolved per D-4: the four un-built primitives ship as ONE packages/ui PR BEFORE any per-app 1D.1/1D.2/1D.3/1D.5 surface starts. Every per-app consumer composes against the same versioned primitives.
1D.1 — Console (platform superadmin)
Hostname: console.restartix.pro. Auth: Clerk. Access gate: is_superadmin (membership in platform_memberships, human-only by CHECK).
Existing UI today: 17 pages (most are nav stubs). See apps/console/app/(dashboard)/ for what exists.
The Console layout, sidebar nav, and the foundational pages (clinics list, clinic detail tabs, audit logs, legal-document templates, platform legal documents) are already wired and consume real backend endpoints. The gap is the depth — most clinic-detail tabs and most catalog/registry pages are mock or stub.
1D.1.A — Identities & access (catalog scope = platform-global)
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| C1 | Users — list humans across platform; view memberships across orgs; block / unblock | GET /v1/users (handler human/handler.go:255) | admin_superadmin | ❌ | ✅ listUsers | Sidebar nav stub /users; no page | OpenAPI |
| C2 | Agents — list AI agent principals (none yet) | none yet — agents table exists, no handler | — | ❌ | ❌ | Sidebar nav stub /agents; no page | Defer until first agent backend ships |
| C3 | Service accounts — list non-human principals (Cat F integrations) | none yet — service_accounts table exists, no handler | — | ❌ | ❌ | Sidebar nav stub /service-accounts; no page | Defer until Cat F backend ships |
| C4 | Platform memberships — grant / revoke superadmin (+ future support_engineer) | POST/GET/DELETE /v1/admin/platform-memberships[/{principalId}] (1D.0) | superadmin | ✅ | ❌ | No nav entry | Backend ready — OpenAPI + api-client bundle with this UI |
| C5 | Permission catalog viewer (read-only — code / resource / action / description per row) | GET /v1/admin/permissions (1D.0) | superadmin | ✅ | ❌ | No nav entry | Backend ready — OpenAPI + api-client bundle with this UI. "Migration introduced" column dropped from the original surface |
| C6 | System role templates viewer (read-only per D-2) — list system role templates (admin, specialist, customer_support) with permissions per template. Editor deferred post-1D until cross-tenant propagation semantics are designed. | GET /v1/admin/role-templates (1D.0) | superadmin | ✅ | ❌ | No nav entry | Backend ready — read-only viewer; mutation endpoints remain deferred per D-2 |
1D.1.B — Tenants
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| C7 | Clinics list — table of orgs; create new clinic dialog | GET /v1/organizations; POST /v1/organizations | superadmin_only | ✅ | ✅ listOrganizations; ❌ createOrganization (missing) | Built — clinics/page.tsx (list) + NewOrganizationDialog | api-client wrapper for create |
| C8 | Clinic overview (clinic detail tab) — stat row, snapshot card, attention nudges, last 5 audit events, quick actions | GET /v1/organizations/{id} (cached orgResource(id, "summary")) | any-member RLS or superadmin | ✅ | ✅ getOrganization | Partial — page renders; stats stay mock at 1D close | Aggregate-stats endpoint deferred per 1D.0 decision (storage tracker doesn't exist; MRR shape unsettled). Lands later in 1E observability / production-launch-readiness |
| C9 | Clinic profile tab — edit name / slug / language; branding & white-label preview (logo, brand color, email-from-name) | PATCH /v1/organizations/{id} | organizations.update | ✅ | ✅ updateOrganization | Partial — name/slug/language editable; branding card is mock | Backend gap — branding columns + asset-upload endpoints not yet defined (universal across shared and dedicated tenancy modes; ships post-foundation) |
| C10 | Clinic members tab — staff directory (no PII gating); add member dialog | GET /v1/organizations/{id}/members; POST /v1/organizations/{id}/members; DELETE /v1/organizations/{id}/members/{principalId}; GET /v1/organizations/{id}/roles | organizations.manage_members | ✅ | ✅ all four | Built — clinics/[id]/members/page.tsx | None |
| C11 | Clinic patients tab (privacy boundary) — aggregate stats default; break-glass elevated read-only patient table | POST /v1/break-glass/sessions; GET /v1/organizations/{id}/patients (under elevation); GET /v1/break-glass/sessions/{id} | superadmin (open break-glass), then RLS via active session | ❌ break-glass; ✅ patients list | ❌ break-glass wrappers; ❌ patients list wrapper | Partial — privacy panel + aggregate stats present; ?bg=active shows mock elevation; real elevation modal not wired | OpenAPI for break-glass, api-client wrappers for both |
| C12 | Clinic plan tab — current plan, active sales overrides, billing form, patient tiers (read-only echo of clinic admin), entitlements toggles | GET /v1/organizations/{id}/subscriptions; GET .../{subId}/overrides; POST .../{subId}/overrides; POST .../{subId}/overrides/{ovId}/revoke; GET /v1/organizations/{id}/billing; PATCH /v1/organizations/{id}/billing; GET /v1/organizations/{id}/entitlements; PATCH /v1/organizations/{id}/entitlements; GET /v1/organizations/{id}/patient-tiers | mix: subscriptions.view_org, subscriptions.manage, superadmin, organizations.manage_billing, RLS for entitlements GET, superadmin for entitlements PATCH | ✅ subs / overrides / billing / entitlements / tiers | ❌ all | Partial — page is mostly mock; no real data | api-client wrappers for all 9 endpoints |
| C13 | Clinic audit tab — per-org audit log slice (metadata always-on; diffs/IPs/bodies gated on break-glass) | GET /v1/audit-logs?organization_id={id} (currently superadmin platform-wide; per-org scoping uses query filter) | superadmin (and break-glass for content) | ❌ | ✅ listAuditLogs | Stub — page exists with mock content; "Open full audit log" button works | OpenAPI for audit-logs |
| C14 | Clinic settings tab — operations + compliance / locale form; domains; legal documents (read-only echo); close clinic (danger). Per D-6: feature_flags JSONB is engineering-internal and NOT exposed in this form. | GET /v1/organizations/{id}/settings; PATCH .../settings; GET .../domains; POST .../domains; DELETE .../domains/{domainId}; POST .../domains/{domainId}/verify; GET .../legal-documents | organizations.update_settings, organizations.manage_domains, organizations.manage_privacy_notice | settings ✅; domains ✅; legal-documents ❌ | settings ✅; domains ✅ (different naming); legal-documents ✅ | Partial — settings + domains wired; legal docs read-only echo; "Close clinic" disabled (no soft-delete endpoint yet) | "Close clinic" → backend gap (no DELETE /v1/organizations/{id} exists; not in 1D scope per glossary) |
| C15 | Onboarding — list orgs in onboarding state, stale-template list | GET /v1/admin/legal-document-templates/stale-orgs | superadmin | ❌ | ✅ listLegalDocumentStaleOrgs | Sidebar nav stub /onboarding; partially shown on legal-document-templates page | OpenAPI for stale-orgs; full onboarding dashboard is in scope but content is sparse |
1D.1.C — Subscriptions & catalog (Console-driven)
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| C16 | Subscriptions (cross-org view) — list all org subscriptions; filter by tier, status | GET /v1/admin/subscriptions[?org_id=&status=&tier_id=&page=&limit=] (1D.0) | superadmin | ✅ | ❌ | Sidebar nav stub /subscriptions; no page | Backend ready — each row enriched with organization_name, organization_slug, active_overrides_count. Per-org mutations on existing per-org routes |
| C17 | Sales overrides (cross-org view) — list active overrides; grant / revoke | GET /v1/admin/subscriptions (1D.0) + per-org override routes (existing) | superadmin | ✅ | ❌ | No nav entry | Backend ready — list is the same aggregator as C16; per-row drill-down via existing per-org POST .../subscriptions/{subId}/overrides + POST .../overrides/{ovId}/revoke |
| C18 | Tiers catalog — list tiers, view detail, see entitlements / limits / version history | GET /v1/tiers; GET /v1/tiers/{id}; GET /v1/entitlements; GET /v1/limit-definitions | auth_only (any signed-in user; superadmin pages just consume) | ✅ | ❌ | Sidebar nav stub /tiers; no page | api-client wrappers for all 4 |
/patient-tiers sidebar stub deleted at 1D.1. | — | — | — | — | /patient-tiers | Sidebar entry removed | |
| C20 | Specialties / Services / Exercises / Forms catalogs (Console superadmin views of per-org content) | none yet — these are clinical domain backends not built in 1A–1C | — | ❌ | ❌ | Sidebar nav stubs /specialties, /services, /exercises, /forms; no pages | Out of 1D scope — backends not built. See Out of 1D scope |
1D.1.D — Communications & integrations
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
/notifications sidebar stub deleted at 1D.1. | — | — | — | — | /notifications | Sidebar entry removed | |
| C22 | SES suppression list (1A.18 — closes in 1E) | none yet — gated by 1E AWS staging | — | ❌ | ❌ | Sidebar nav stub /suppression; no page | Defer to 1E |
| C23 | Announcements | none yet — no backend | — | ❌ | ❌ | Sidebar nav stub /announcements; no page | Out of 1D scope — no backend |
/webhooks sidebar stub deleted at 1D.1. | — | — | — | — | /webhooks | Sidebar entry removed | |
/connectors sidebar stub deleted at 1D.1. | — | — | — | — | /connectors | Sidebar entry removed | |
| C26 | Platform service providers (Cat A — 1C.2 superadmin write surface) — list / create / update / delete platform-default + per-org-override providers (email/ses, storage/aws_s3, auth/clerk) | GET /v1/admin/platform-service-providers; POST /v1/admin/platform-service-providers; GET /v1/admin/platform-service-providers/{id}; PATCH /v1/admin/platform-service-providers/{id}; DELETE /v1/admin/platform-service-providers/{id} (mounted at routes.go:250) | platform providers.manage or superadmin | ❌ | ❌ | No nav entry — backend shipped 1C.2 but Console UI not yet planned | CONFIRMED IN 1D.1 SCOPE per D-5. OpenAPI + api-client + new sidebar entry. Load-bearing for dedicated-tier rollout (per-org Cat A overrides) |
| C27 | AI models registry (1C.8) — list / create / get / update / pricing | GET /v1/admin/ai-models; POST /v1/admin/ai-models; GET /v1/admin/ai-models/{id}; PATCH /v1/admin/ai-models/{id}; POST /v1/admin/ai-models/{id}/pricing (mounted at routes.go:261) | superadmin or platform ai_models.manage | ❌ | ❌ | No nav entry — backend shipped 1C.8 | CONFIRMED IN 1D.1 SCOPE per D-5. OpenAPI + api-client + new sidebar entry. Load-bearing for AI cost configuration |
1D.1.E — Observability
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| C28 | Audit logs (cross-tenant) — search, paginate, sort, filter by action / entity_type / action_context / status_class / org / actor / actor_type / date range | GET /v1/audit-logs; GET /v1/organizations?ids=...; GET /v1/users?ids=... (for chip resolution) | superadmin | ❌ | ✅ listAuditLogs, listOrganizations, listUsers | Built — audit-logs/page.tsx with full filter chip UI | OpenAPI for audit-logs |
| C29 | Break-glass sessions — list active + recent sessions; close session; view session detail (with reason, scope, opened_at, expires_at) | GET /v1/break-glass/sessions; GET /v1/break-glass/sessions/{id}; POST /v1/break-glass/sessions; POST /v1/break-glass/sessions/{id}/close | RLS (self / break_glass.manage / audit_log.view_org) | ❌ | ❌ | Sidebar nav stub /break-glass; no page | OpenAPI + api-client. Foundational — required before C11 can wire real elevation flow |
| C30 | System health — operational platform dashboard (concurrent users, request rate, latency, error rate, dependencies, partition runway) | none — page is fully mock | — | ❌ | ❌ | Built (mock) — system-health/page.tsx | MOVED TO 1E per D-11. Page stays mock at 1D close; real wiring lands in 1E observability scope (consumes staging KMS/S3/RDS/SES artifacts that exist after 1E.3) |
1D.1.F — Platform configuration
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| C31 | Legal-document templates (platform-wide) — list latest per (document_type, locale); publish new version (one row per locale at version MAX+1); see stale-orgs | GET /v1/admin/legal-document-templates; POST /v1/admin/legal-document-templates; GET /v1/admin/legal-document-templates/stale-orgs | superadmin | ❌ | ✅ all three | Built — legal-document-templates/page.tsx + /publish | OpenAPI |
| C32 | Platform legal documents (consent purposes — platform_terms, platform_privacy_notice) — list versions; edit + publish new version (triggers re-consent platform-wide) | GET /v1/admin/platform-consent-purpose-versions; POST /v1/admin/platform-consent-purpose-versions | superadmin | ❌ | ✅ both | Built — platform-legal-documents/page.tsx + [code] editor | OpenAPI |
organization_settings.feature_flags is engineering-internal (staged-rollout JSONB), distinct from entitlements. Not a UI surface — Console /feature-flags sidebar stub deleted at 1D.1; field also dropped from C14/L2 settings forms. | — | — | — | — | /feature-flags | Sidebar entry removed | |
messages/{en,ro}.json); no locales DB table. Console /locales sidebar stub deleted at 1D.1. | — | — | — | — | /locales | Sidebar entry removed | |
| C35 | Settings (Console superadmin's own preferences) | GET /v1/me; PATCH /v1/me | none | ✅ | ✅ | Sidebar nav stub /settings; no page | None — straightforward to wire |
| C36 | Sales / Marketing / Compliance misc dashboards | none yet | — | ❌ | ❌ | None | Out of 1D scope |
1D.2 — Clinic admin UI (org self-service)
Hostname: {slug}.clinic.restartix.pro. Auth: Clerk + org-scoped via hostname → org-id cookie. Access gate: structural (membership row in organization_memberships for the resolved org).
Per foundation.md:1255–1280, all 1D.2 surfaces are deferred to a clinic-app refresh that has not started; the backend contracts are frozen and stable, and "no Go changes needed when this lands."
Existing UI today: 4 wired pages + 13 nav stubs. Wired pages are (dashboard)/page.tsx (legal-docs onboarding card), (dashboard)/legal-documents/page.tsx, (dashboard)/legal-documents/[type]/page.tsx, sign-in. Everything else is a sidebar entry without a page.
1D.2.A — Tenant configuration
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| L1 | Org profile editor — name, slug (read-only), language, default timezone | GET /v1/organizations/{id}; PATCH /v1/organizations/{id} | organizations.update | ✅ | ✅ | Sidebar nav stub /organizations; no page | None |
| L2 | Organization settings — marketing prefs, retention override, support locale, telerehab toggle (entitlement-mirrored read-only). Per D-6: feature_flags JSONB is engineering-internal and NOT exposed in this form. | GET /v1/organizations/{id}/settings; PATCH .../settings | organizations.update_settings | ✅ | ✅ | Sidebar nav stub /settings; no page | None |
| L3 | Custom domains — list, add, verify (DNS TXT record), remove | GET /v1/organizations/{id}/domains; POST .../domains; POST .../domains/{domainId}/verify; DELETE .../domains/{domainId} | organizations.manage_domains | ✅ | ✅ (naming differs: addDomain etc.) | No nav entry; tab on Console clinic-detail page exists | None |
| L4 | Billing (read-only on Clinic) — current plan, period, contact, renewal | GET /v1/organizations/{id}/billing | organizations.manage_billing | ✅ | ❌ | No nav entry | api-client wrappers |
| L5 | Locations (1B.5) — list, create, update, close (status), delete | GET /v1/organizations/{id}/locations; POST .../locations; GET .../locations/{locationId}; PATCH .../locations/{locationId}; DELETE .../locations/{locationId} | locations.manage (mutations); RLS for read | ❌ | ❌ | Sidebar nav stub /locations; no page | OpenAPI gap (full domain) + api-client gap (full domain) |
1D.2.B — Members, roles, invitations
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| L6 | Members — staff directory list, role-change dropdown, remove | GET /v1/organizations/{id}/members; POST .../members; DELETE .../members/{principalId}; GET .../roles | organizations.manage_members | ✅ | ✅ | Sidebar nav stub /users; no page (Console clinic-detail tab is built) | None |
| L7 | Roles editor — edit cloned system roles + create custom roles + permissions checklist | none yet — roles + role_permissions write endpoints not exposed | organizations.manage_roles (TBD) | ❌ | ❌ | No nav entry | Backend gap — full domain missing. See Gaps § G-3 |
| L8 | Staff invitations (1B.12) — list (pending / accepted / revoked); invite form (email + role); revoke; resend | POST /v1/organizations/{id}/staff-invitations; GET .../staff-invitations; GET .../invitations/{inviteId}; POST .../invitations/{inviteId}/revoke; POST .../invitations/{inviteId}/resend | organizations.manage_members | ❌ | ❌ | No nav entry | OpenAPI gap (full domain) + api-client gap (full domain) |
| L9 | Patient invitations (1B.12) — list; invite form (email + tier); revoke; resend | POST /v1/organizations/{id}/patient-invitations; GET .../patient-invitations; (shared revoke / resend with L8) | patients.manage | ❌ | ❌ | No nav entry | Same as L8 |
| L10 | Patient share-links (1B.12) — mint form (tier, max_uses, expires_at, note); list with copy / QR; revoke | POST /v1/organizations/{id}/share-links; GET .../share-links; GET .../share-links/{shareLinkId}; POST .../share-links/{shareLinkId}/revoke | organizations.manage_share_links | ❌ | ❌ | No nav entry | OpenAPI gap + api-client gap (resolveShareLink exists but admin endpoints don't) |
1D.2.C — Patients & consents
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| L11 | Patients list — search, paginate, sort, archive | GET /v1/organizations/{id}/patients; POST .../patients; GET .../patients/{patientId}; DELETE .../patients/{patientId} | patients.view, patients.manage | ✅ | ❌ | Sidebar nav stub /patients; no page | api-client wrappers (whole domain) |
| L12 | Patient detail — profile (mask if profile_shared = false); consents trail; subscription; impersonation history; appointments (post-foundation); forms (post-foundation) | combination of L11 + L13 + L14 + L15 | per surface | mixed | mixed | No page | composes L11 / L13 / L14 / L15 |
| L13 | Per-patient consents (staff view + staff action) — history; grant on patient's behalf; withdraw on patient's behalf | GET /v1/organizations/{id}/patients/{patientId}/consents; POST .../consents; POST .../consents/{consentId}/withdraw | consents.view_org, consents.manage | ❌ | ❌ | No nav entry | OpenAPI gap (full consent domain) — see Gaps § G-1 |
| L14 | Per-patient subscription — view active tier; cancel | GET /v1/organizations/{id}/patient-subscriptions; GET .../patient-subscriptions/{subId}; POST .../patient-subscriptions/{subId}/cancel | patient_subscriptions.view_org, patient_subscriptions.manage | ✅ | ❌ | No nav entry | api-client wrappers |
| L15 | Per-patient impersonation history — staff sessions on this patient (sidebar of patient detail) | GET /v1/organizations/{id}/patient-impersonation-sessions?patient_id=... | RLS (patients.manage) | ❌ | ❌ | No nav entry | OpenAPI + api-client (whole domain) |
| L16 | Patient impersonation modal (open session with reason) | POST /v1/organizations/{id}/patient-impersonation-sessions; POST .../patient-impersonation-sessions/{sessionId}/close; GET .../patient-impersonation-sessions | patients.impersonate, patients.manage | ❌ | ❌ | No nav entry | Same as L15 |
1D.2.D — Patient tier configuration
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| L17 | Patient tiers (1B.4) — list, add, edit (entitlements + limits picker), archive (is_active=false) | GET /v1/organizations/{id}/patient-tiers; POST .../patient-tiers; GET .../patient-tiers/{tierId}; PATCH .../patient-tiers/{tierId} | patient_tiers.manage; RLS for read | ✅ | ❌ | Sidebar nav stub doesn't exist on Clinic; Console has stub /patient-tiers (cross-tenant) | api-client wrappers; nav entry on Clinic |
1D.2.E — Integrations
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| L18 | Outbound webhook subscriptions (Cat C — 1C.4) — create (target URL + event filters); list (filterable by status); update (URL, events, status); revoke (soft-delete); rotate signing secret (dual-secret); list deliveries; fire test delivery | POST /v1/organizations/{id}/outbound-webhook-subscriptions; GET .../outbound-webhook-subscriptions; GET .../outbound-webhook-subscriptions/{subscriptionId}; PATCH .../{subscriptionId}; DELETE .../{subscriptionId}; POST .../{subscriptionId}/regenerate-secret; GET .../{subscriptionId}/deliveries; POST .../{subscriptionId}/test | organizations.manage_webhooks + EnforceLimit(max_webhook_subscriptions) | ❌ | ❌ | No nav entry | OpenAPI gap (full domain — 8 ops) + api-client gap (full domain) |
| L19 | Connected accounts (Cat B — 1C.5; framework-only, no consumers yet) — public catalog (GET /v1/integration-services); per-org create / list / get / update / delete / test | GET /v1/integration-services; POST /v1/organizations/{id}/integrations; GET /v1/organizations/{id}/integrations; GET /v1/organizations/{id}/integrations/{integrationId}; PATCH .../{integrationId}; DELETE .../{integrationId}; POST .../{integrationId}/test | organizations.manage_integrations (per-org); rate-limited public catalog | ❌ | ❌ | No nav entry | OpenAPI gap (full domain — 7 ops) + api-client gap. First Cat B catalog row + connector impl + OAuth callback handler is deferred to first F-tier consumer per foundation.md:1C.5, so the UI may be stub-only at 1D close |
1D.2.F — Compliance
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| L20 | Legal documents (1B.10) — list (terms + privacy_notice cards); editor with placeholders + toggleable sections; preview; publish (creates consent_purpose_version row → triggers re-consent); refresh source template | GET /v1/organizations/{id}/legal-documents; GET .../legal-documents/{type}; PUT .../legal-documents/{type}; POST .../legal-documents/{type}/preview; POST .../legal-documents/{type}/publish; POST .../legal-documents/{type}/refresh-source-template | organizations.manage_privacy_notice | ❌ | ✅ all | Built — legal-documents/page.tsx + legal-documents/[type]/page.tsx | OpenAPI gap (api-client wraps via reverse-engineered shape) |
| L21 | Per-org audit log viewer (1A.1 backend; 1B equivalent of C13) — search, filter, paginate, detail Sheet | GET /v1/audit-logs?organization_id={current_org_id} | audit_log.view_org | ❌ | ✅ listAuditLogs | Sidebar nav stub /audit-log; no page | OpenAPI; nav-stub → page |
| L22 | Break-glass active-session banner (1B.11) — show when platform staff is currently elevated against this org | GET /v1/break-glass/sessions?org_id={current_org_id} | RLS via audit_log.view_org | ❌ | ❌ | No surface | OpenAPI + api-client; persistent banner pattern (1D.4) |
1D.2.G — Clinical (post-foundation; not in 1D scope)
The following surfaces are listed for completeness but their backends are F-tier features, not foundation:
| Surface | Foundation status |
|---|---|
| Calendar (month / week views) | F-tier (appointments domain not built in 1A–1C) |
| Patients clinical detail (medical record, allergies, episodes) | F-tier |
| Forms designer + filler | F-tier |
| Treatment plans (templates + assignment) | F-tier |
| Exercise library editor | F-tier |
| Specialists (list + license tracking + schedule) | F-tier |
| Services / Offerings catalog | F-tier (also subject to deferred services → offerings rename per glossary) |
| Specialties | F-tier |
| Segments | F-tier |
| Custom fields | F-tier |
| PDF templates | F-tier |
| Automations | F-tier |
| Reports | F-tier |
| Daily.co video calls | F-tier |
These appear as nav stubs (/calendar, /treatment-plans, etc.) in the existing Clinic sidebar; they should remain stubs at 1D close.
1D.3 — Patient Portal (self-service)
Hostname: {slug}.portal.restartix.pro. Auth: Clerk. Access gate: structural (patients row in resolved org). Re-consent gate (RequireCurrentConsents) blocks all per-org pages until missing consent versions are accepted.
Existing UI today: 7 wired pages, 8 nav stubs. Wired: sign-in, sign-up, landing (page.tsx), /join/[code], /onboard, (patient)/dashboard (placeholder), (patient)/consents.
1D.3.A — Identity & onboarding
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| P1 | Sign-up + sign-in (Clerk-hosted) | none — Clerk | none | n/a | n/a | Built — sign-in/[[...sign-in]]/page.tsx, sign-up/[[...sign-up]]/page.tsx | None |
| P2 | Public share-link landing /join/[code] (anonymous, no Clerk yet) — branded clinic name + tier; CTA → Clerk sign-up; stores code in httpOnly cookie | GET /v1/public/share-links/{code} | none | partial (resolveShareLink in OpenAPI) | ✅ resolveShareLink | Built — join/[code]/page.tsx | None |
| P3 | Onboard /onboard (two-step) — Step 1: profile + platform consents (POST /v1/me/patient-profile); Step 2: per-clinic membership + org consents (POST /v1/portal/onboard); patient-invite banner if pending | POST /v1/me/patient-profile; POST /v1/portal/onboard; GET /v1/me; GET /v1/me/pending-invitations; GET /v1/consent-purposes | none (auth_only) | partial (portalOnboard, setupMyPatientProfile) | ✅ setupPatientProfile, onboardPatient, listMyPendingInvitations, listConsentPurposes, getMe | Built — onboard/page.tsx (multi-step form) | OpenAPI for pending-invitations + consent-purposes |
| P4 | Re-consent modal (blocking) — required-consents discovery + accept | GET /v1/me/required-consents; POST /v1/me/consents | none (auth_only) | ❌ | ✅ listMyRequiredConsents, grantMyConsent | Built — wired in (patient)/layout.tsx | OpenAPI |
| P5 | Sign-out + locale selector + theme | none — Clerk + i18n | none | n/a | n/a | Built | None |
1D.3.B — Account self-service
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| P6 | My profile (portable patient profile — DOB, blood type, allergies, etc.; recipient-timezone IANA picker per 1A.18's 1D.3 close) | GET /v1/me/patient-profile; PATCH /v1/me/patient-profile | RequirePrincipalRLS | ❌ | ✅ existing wrappers (need verification) | Sidebar nav stub /profile; no page | OpenAPI; recipient-timezone picker is the close-out of 1A.18 per foundation.md:199 |
| P7 | My consents (per-org + platform history; grant + withdraw self-toggleable purposes; "delete account to revoke" copy for non-withdrawable) | GET /v1/consent-purposes; GET /v1/me/consents; POST /v1/me/consents; POST /v1/me/consents/{consentId}/withdraw | none (auth_only) | ❌ | ✅ all four | Built — (patient)/consents/page.tsx | OpenAPI |
| P8 | My subscription — view active tier, included features, limits, period dates | GET /v1/me/patient-subscription | none (auth_only) + RequireCurrentConsents | ❌ | ❌ | Sidebar nav stub /subscription; no page | OpenAPI gap + api-client gap |
| P9 | My documents (foundation-scope: legal docs signed at signup; clinical docs are F-tier) | GET /v1/organizations/{id}/legal-documents (read-only echo) — or per-clinic privacy notice viewer | organizations.manage_privacy_notice (or RLS on consent_purpose_versions) | ❌ | ✅ listOrganizationLegalDocuments | Sidebar nav stub /documents; no page | OpenAPI; clarify scope |
| P10 | Privacy — view clinic + platform privacy notices signed at signup | derived from P7 + P9 | none | n/a | n/a | Sidebar nav stub /privacy; no page | UI work only |
| P11 | Access history (1B.13 — backend shipped) — per-clinic list of staff impersonation sessions on me; who, when, reason, duration | GET /v1/me/patient-impersonation-sessions[?organization_id=&only_active=&limit=&offset=] | RequirePrincipalRLS | ❌ | ❌ | No nav entry | OpenAPI + api-client |
| P12 | Pending invitations banner (on /onboard) — show "you've been invited to Acme Clinic" when a pending patient invite exists for (principalID, currentOrgID) | GET /v1/me/pending-invitations | none (auth_only) | ❌ | ✅ listMyPendingInvitations | Built in /onboard | OpenAPI |
1D.3.C — Clinical (post-foundation; out of 1D scope)
Sidebar entries /appointments, /exercises, /treatment-plan, /forms are F-tier consumer surfaces. They remain stubs at 1D close.
1D.5 — Cross-Org Account Surface (new app)
Hostname: account.restartix.pro (final name TBD per foundation.md:1326). Auth: Clerk. No org-resolver, no X-Organization-ID header — runs on the platform-portable session (set_app_principal with no org), so existing RLS policies (consents_select_self, patients_select_self, patient_subscriptions_select_self) return cross-org unions via the current_app_org_id() IS NULL branch.
No app exists today — apps/account/ is not yet scaffolded. Per foundation.md:1323, this surface must ship before staging cuts over (1E.3 dependency).
| # | Surface | Backend endpoints | Permission | OpenAPI | api-client | Existing UI | Gap |
|---|---|---|---|---|---|---|---|
| A1 | Hostname provisioning — DNS, ACM cert, Route53 entry; same shape as console.restartix.pro | n/a — infra | n/a | n/a | n/a | None | Infra work; close in 1E |
| A2 | App scaffold — new apps/account/ Next.js app or extend an existing one with a no-org-resolver layout | n/a — code scaffold | n/a | n/a | n/a | None | Scaffold work |
| A3 | My profile (cross-org portable, read + edit) | GET /v1/me/patient-profile; PATCH /v1/me/patient-profile | RequirePrincipalRLS | ❌ | ✅ | None | Same as P6 |
| A4 | My clinics (list patient_org_ids with each clinic's name, primary contact, DPO email for DSAR routing) | GET /v1/me/clinics (new — ships in 1D OpenAPI catch-up pass per D-8) — returns {org_id, name, slug, primary_contact, dpo_email, ...} per clinic the patient is at; field-filtered subset of organization_billing (no other billing data leaks) | RequirePrincipalRLS (no org-context required) | ❌ | ❌ | None | Resolved — endpoint to ship in OpenAPI catch-up; A4 unblocked. Required by GDPR DSAR-routing hard rule |
| A5 | My consents (cross-org view; per-clinic toggles preserved; withdraw at clinic A leaves clinic B intact) | GET /v1/me/consents; POST /v1/me/consents; POST /v1/me/consents/{consentId}/withdraw (cross-org via no org-id header) | RequirePrincipalRLS | ❌ | ✅ | None | OpenAPI; verify cross-org behavior of listMyConsents when no X-Organization-ID |
| A6 | Cross-org data export request (GDPR Art. 15 / 20) | none yet — F11 implementation | — | ❌ | ❌ | None | Out of 1D scope — F11 backend not built |
| A7 | Account deletion request (full GDPR erasure) | none yet — F11 implementation | — | ❌ | ❌ | None | Out of 1D scope — F11 backend not built |
| A8 | Access history (cross-org impersonation sessions) | GET /v1/me/patient-impersonation-sessions (no org filter) | RequirePrincipalRLS | ❌ | ❌ | None | Same as P11 |
| A9 | Locale + theme + sign-out | n/a | n/a | n/a | n/a | None | UI work |
Acceptance test required at 1D.5 close (foundation.md:1336): a patient enrolled at two clinics signs in to account.restartix.pro, sees both clinics, withdraws marketing at clinic A, verifies clinic B still granted, triggers cross-org export, signs out. Add to 1E.2's setup-a-clinic acceptance before staging cutover.
Gaps to flag
G-1 OpenAPI coverage
OpenAPI is the contract every UI engineer (and every external Cat F integrator) consumes. Current state: apps/docs/openapi.yaml covers about half the routes mounted in routes.go. Domains entirely missing from OpenAPI:
| Domain | Endpoints | Surface impact |
|---|---|---|
| Locations | 5 | L5 |
| Patient impersonation | 4 | C11 (modal), L15, L16, P11, A8 |
| Outbound webhooks | 8 | L18 |
| Connected accounts (Cat B) | 7 | L19 |
| Integration services catalog | 1 | L19 |
| Platform service providers | 5 | C26 |
| AI models | 5 | C27 |
| Break-glass | 4 | C29, C11, L22 |
| Audit logs | 1 (with rich filters) | C13, C28, L21 |
| Invitations (staff + patient + share-link admin) | 8 | L8, L9, L10 |
| Consents (mine + platform + per-patient + purposes catalog) | 9 | C32, L13, P3, P4, P7 |
| Legal documents (per-org + admin templates) | 9 | C31, L20 |
Patient profile (/v1/me/patient-profile) | 2 | P6, A3 |
| Patient subscriptions (mine) | 1 (/v1/me/patient-subscription) | P8 |
/v1/me/required-consents, /v1/me/consents mutations, /v1/me/pending-invitations, /v1/me/patient-impersonation-sessions | 7 self-context ops | P3, P4, P7, P11, P12 |
Every "❌ OpenAPI" cell in the per-app tables traces to one of these. OpenAPI sync is the single highest-leverage unblock — it removes ambiguity for ~71 operations at once.
G-2 api-client coverage
packages/api-client/src/client.ts is hand-written, not generated. Current state: ~38 wrappers for ~105 backend endpoints (~36% coverage). The wrappers that exist are mostly the ones consumed by the already-built Console pages (audit, legal-documents, organizations, members, consent self-toggles).
Domains with zero api-client wrappers (and therefore every consuming UI surface needs the wrapper before any UI work can start):
- Patients (4 ops)
- Patient subscriptions (3 ops)
- Patient tiers (4 ops)
- Locations (5 ops)
- Webhooks (8 ops)
- Integrations (7 ops)
- Platform service providers (5 ops)
- AI models (5 ops)
- Break-glass (4 ops)
- Patient impersonation (4 ops)
- Invitations (5 ops, both staff + patient)
- Org subscriptions / overrides (8 ops)
- Org billing + entitlements (4 ops)
- Tiers / entitlements / limit-definitions catalog (4 ops)
Naming-convention drift — even covered endpoints have inconsistent ops IDs between OpenAPI (getCurrentUser, addOrganizationDomain) and client (getMe, addDomain). Resolved per D-9: the OpenAPI spec is renamed to client-style names (concise, already in use across all built UI) during the catch-up pass. Convention is documented in apps/docs/reference/api-conventions.md.
G-3 Missing endpoints
Backend gaps surfaced by this audit (UI needs them, they don't exist yet). Items resolved by the 2026-05-07 decisions are noted inline; remaining items need explicit human review before any UI consumer can ship — none of these are silently added in the OpenAPI catch-up pass.
List endpoint for permission catalog (C5)— RESOLVED in 1D.0:GET /v1/admin/permissionsshipped (superadmin-only, returns code/resource/action/description/created_at sorted by code withCOLLATE "C"). "Migration introduced" column dropped from the original surface — not on the table. OpenAPI entry bundles with the C5 UI consumer.System role templates list endpoint (C6 read-only viewer)— RESOLVED in 1D.0:GET /v1/admin/role-templatesshipped (superadmin-only, returns each system template with its permissions array resolved server-side).- System role template propagation editor (C6 editor) — deferred post-1D per D-2.
Platform memberships management (C4)— RESOLVED in 1D.0:POST/GET/DELETE /v1/admin/platform-memberships[/{principalId}]shipped (superadmin-only). Role enum validated against{superadmin, support_engineer}. Every grant + revoke audit-logged withaction_context = 'platform_membership_change'.Cross-org subscriptions aggregator (C16, C17)— RESOLVED in 1D.0:GET /v1/admin/subscriptions[?org_id=&status=&tier_id=&page=&limit=]shipped (superadmin-only). Each row enriched withorganization_name,organization_slug,active_overrides_count. Paginated via the standardapiqueryenvelope. Per-org override mutations remain on existing per-org routes.- Per-org aggregate stats (C8 — Clinic overview) — DEFERRED per 1D.0 decision: storage tracker doesn't exist; MRR shape unsettled; no production-blocking gap. Cards ship mock at 1D close; the endpoint lands later in 1E observability or production-launch-readiness. System-health page (C30) deferred to 1E per D-11.
- Custom roles editor (L7) — same as item 3 but org-scoped; no
rolesmutation surface in any handler. Status: still a backend gap. Notification templates editor (C21)— resolved per D-3: stay migration-managed; no admin endpoints, sidebar stub removed.- DPO contact for cross-org "my clinics" (A4) — RESOLVED per D-8: ship
GET /v1/me/clinicsreturning{org_id, name, slug, primary_contact, dpo_email, ...}per clinic; field-filtered subset oforganization_billing(no other billing data leaks). Lands in the OpenAPI catch-up pass. - GDPR Art. 15 / 20 export + Art. 17 deletion (A6, A7) — F11 scope, not foundation. Listed as Out of 1D scope.
G-4 UI primitive gaps
Already covered in § 1D.4. Restated for visibility:
| Primitive | Used by surfaces | Status |
|---|---|---|
<RequirePermission /> helper | every gated control across all 3 apps (~50+ uses) | Not built |
| Toast / notification system | every mutation feedback (publish, save, delete, copy, etc.) | Not built |
| Server-validation rendering (422 → form field errors) | every form (~25 across apps) | Not built |
| Empty / loading / error states standardised | every list / detail page | Not built |
| Persistent banner pattern (for break-glass, re-consent prompts) | C11, L22, P4 | Built (re-consent only) — generalise the pattern |
| QR-code component (for share-link mint UI) | L10 | Not built |
| Block-based editor (for legal-document placeholders / sections) | L20 (built via custom form), C31 / C32 | Built (form-based) — no general block editor |
System-only — no UI needed (record so future engineers don't re-ask)
Backend functionality that exists for cron / internal / observability purposes; explicitly out of UI scope:
cmd/audit-partition-roll(1A.1, P41) — monthly partition pre-creation cron. System-only.cmd/check-capabilities(1C.1) — CI guard verifying every external call goes through a Capability. System-only.cmd/check-providers(1C.2) — provider healthcheck cron. System-only.cmd/check-cata-resolution(1C.2) — CI guard enforcing platform-providers resolution. System-only.cmd/check-events-registry(1C.3) — CI guard for events registry. System-only.cmd/dump-events-registry(1C.3) — generatesapps/docs/architecture/_generated/events-catalog.md. System-only.cmd/check-inbound-webhooks(1C.6) — AST-based CI guard. System-only.cmd/check-classification— classification registry CI guard (1A.13). System-only.cmd/check-soup— SOUP inventory CI guard. System-only.internal/core/inboundwebhooks/dedup/(1C.6 — Cat D framework) — monthly-partitioned dedup table; partition roll +WasProcessed/MarkProcessedrepo helpers. System-only at framework layer; per-provider handlers (Stripe, Daily.co, Clerk) ship with their consumers and may add provider-specific surfaces, none of which are foundation.internal/core/events/registry.go+events.Bus(1A.9, 1C.3 — Cat E) — internal event bus. System-only; no UI for "fire event" or "list events."internal/core/notify/dispatcher + EmailChannel + FakeChannel + outbox cron — internal email dispatcher. System-only at the dispatcher layer; the templates seeded forMemberInviteandBreakGlassOpenedmay surface in C21 if templates become editable.internal/core/webhooks/outbox dispatcher (1C.4) — event-bus subscriber that fans events to clinic-configured webhook subscriptions. System-only at the dispatcher layer; subscription management UI is L18.internal/core/metering/+ crons (1C.7) — metering counters + quota enforcement. System-only at counter layer; usage display surfaces in patient subscription (P8) and clinic billing (L4).internal/core/cache/Redis cache-aside (P45) +unstable_cache(P42) — caching layers. System-only.set_app_principal_id/current_app_principal_id()/current_app_has_permission()RLS GUCs and helpers — RLS plumbing. System-only.set_app_impersonation_session_id(1B.13) — RLS impersonation context GUC. System-only.- Seeded migration data: every system role template, every seeded permission, every consent purpose catalog row, every entitlement / limit definition, every notification template, every classification entry, every events registry entry. Mutated only by future migrations. None of these need a CRUD UI; some get read-only viewers (C5 permission catalog, C18 tiers / entitlements / limits catalog, the consent purposes side-panel on L13/P7).
telemetrypackage — pseudonymised forwarding to observability backend. System-only.cmd/check-soup+apps/docs/reference/soup.md— SOUP inventory tracker. System-only.
Out of 1D scope
UI surfaces whose backend isn't built yet, or whose backend is explicitly deferred to a feature consumer:
Backends not built (F-tier features)
These appear as stub nav entries today and remain stubs at 1D close:
- Appointments (Clinic calendar, public booking, patient view, video calls) — F2, F3, F5
- Forms (template designer, filler, signature capture) — F-tier
- Treatment plans (templates, assignment, library browse, session execution) — F-tier
- Exercise library (editor, instructions, contraindications, video upload) — F-tier
- Specialists (list, license tracking, schedule editor, weekly hours, overrides) — F-tier
- Services / Offerings (catalog editor, service-specialist matrix) — F-tier; subject to deferred
services→offeringsrename - Specialties (list, editor) — F-tier
- Segments (cohort builder, rules from forms / profiles / appointments) — F-tier
- Custom fields (definitions, versioning, profile sync) — F-tier
- PDF templates (block-based editor, components library) — F-tier
- Automations (rule builder, execution log) — F-tier
- Reports — F-tier
- Billing / invoicing (clinic-side billing dashboard, F12) — F12
- Patient marketplace payments (Stripe Connect, KYB / KYC, PSD2) — strategic feature, not foundation
- GDPR Art. 15 / 17 / 20 export & deletion flows — F11 (A6, A7 in 1D.5 inventory)
Backends deferred to first consumer
- Cat B Connected Account first catalog row + Connector impl + OAuth callback handler — deferred per
foundation.md:1C.5. The framework ships at 1C.5; the first F-tier consumer (e.g. Google Calendar for F2 appointments) brings the first catalog row. L19 may be stub-only at 1D close. - Cat D Inbound Webhook per-provider handlers (Stripe, Daily.co, Clerk webhooks) — deferred per
foundation.md:1C.6. Framework ships; per-provider handlers ship with their F-tier consumers. - AI capability provider impls (Anthropic, OpenAI text generation, vision, embeddings) — 1C.8 ships seams with no provider impls. C27 (AI models registry) UI is in scope; actually-using-an-AI-model is post-foundation.
Decisions (resolved 2026-05-07)
Settled the day after the audit landed. Each entry locks the outcome that the per-app plans now consume.
D-1 Console cross-tenant nav stubs /webhooks, /connectors, /patient-tiers — REMOVE
Per CLAUDE.md → Foundation Discipline, cross-tenant aggregators built before a stated platform-ops use-case are speculation. Webhooks (Cat C) and Connectors (Cat B) stay clinic-side; patient tiers stay clinic-side. The three Console sidebar entries are removed at 1D.1 — no aggregator endpoints, no Console pages. If a real platform-ops need surfaces later, it lands as an explicit ADR-worthy request.
Affects rows: C19 (removed), C24 (removed), C25 (removed).
D-2 System role template editor — READ-ONLY VIEWER ONLY at 1D close
C6 ships as a read-only viewer of system role templates (mirroring C5's permission-catalog viewer). The full editor — which requires designing cross-tenant propagation semantics (grants propagate to all org clones; revocations do not) — is deferred post-1D. C5 is unchanged. Custom-roles-per-org (L7) stays as a flagged backend gap.
Affects rows: C6 (scope reduced to read-only).
D-3 Notification templates — MIGRATION-MANAGED, remove /notifications stub
Foundation templates stay seeded by migration. F-tier features that introduce new notification categories add per-template editing if their UX requires it. The Console /notifications sidebar stub is removed at 1D.1.
Affects rows: C21 (removed); section "System-only" already records the dispatcher / outbox as system-only.
D-4 1D.4 primitive ordering — SHIP packages/ui PR FIRST
The four un-built primitives (<RequirePermission />, toast, server-validation renderer, standardised empty/loading/error states) ship as ONE packages/ui PR before any per-app 1D.1/1D.2/1D.3/1D.5 surface starts. Every per-app consumer composes against the same versioned primitives. This is stricter than the audit's parallel-prototype recommendation; the rationale is that prototypes hardened on one app's first surface diverge from the version a second app starts against.
Affects: 1D.4 sequencing line in foundation.md.
D-5 1C.2 platform-service-providers + 1C.8 ai-models Console pages — IN 1D.1 SCOPE
Both shipped Console superadmin admin endpoints in 1C; neither had a 1D.1 sidebar entry in the original foundation.md:1219–1254 scope. They're load-bearing for dedicated-tier rollout (Cat A per-org overrides) and AI cost configuration; without UI, superadmin can't drive these flows. Add sidebar entries + CRUD pages to 1D.1.
Affects rows: C26 (confirmed in scope), C27 (confirmed in scope). foundation.md 1D.1 list needs an explicit append.
D-6 "Feature flags" — NOT A UI SURFACE
Per CLAUDE.md taxonomy, organization_settings.feature_flags (staged-rollout JSONB) is engineering-internal — distinct from the 1C.9 entitlements rename (which is the user-facing per-tenant capability toggle). It does NOT surface in the Console (C33 removed) AND it does NOT surface in the Org Settings UI on either Console or Clinic (C14 / L2 lose the field from their UI scope; the PATCH .../settings endpoint may still accept it for engineering use, but the form does not expose it).
Affects rows: C33 (removed); C14 (drop feature_flags from form); L2 (drop feature_flags from form).
D-7 "Locales" — REMOVE
i18n config (next-intl messages/en.json, messages/ro.json) is system-only; no locales DB table exists. The Console /locales sidebar stub is removed at 1D.1.
Affects rows: C34 (removed).
D-8 Cross-org "My clinics" DPO contact — SHIP GET /v1/me/clinics
Required by GDPR ("DSAR routing flows through the clinic, never the platform" — CLAUDE.md hard rule). A generic "contact your clinic" CTA defeats the rule. 1D ships a small endpoint that, with no X-Organization-ID header, returns the patient's clinics with {org_id, name, slug, primary_contact, dpo_email, ...} for each patient_org_id. DPO/contact fields are sourced from organization_billing but exposed read-only and field-filtered on this endpoint (no other billing data leaks). Land in the OpenAPI catch-up pass; A4 then consumes it.
Affects rows: A4 (resolved — endpoint to ship); G-3 entry #8 (resolved). New endpoint added to routes.go + handler + OpenAPI + api-client.
D-9 OpenAPI / api-client naming alignment — CLIENT-STYLE NAMES; pass during OpenAPI catch-up
The built UI consumes the concise client-style names (getMe, addDomain, onboardPatient). Renaming the spec is the smaller, less-invasive direction (no consumer code changes vs. ~38 client wrappers + every app call site). The OpenAPI catch-up agent renames getCurrentUser → getMe, addOrganizationDomain → addDomain, etc. as part of the same pass; the convention is added to apps/docs/reference/api-conventions.md.
D-10 services → offerings — DEFER
Glossary locks the rename target. The actual file/code rename waits until clinical services (F-tier) is built; no 1D action. Console /services sidebar stub stays as an F-tier nav stub today and is removed per D-12 at 1D close.
D-11 System health (C30) — 1E, NOT 1D
The page consumes staging deployment artifacts (KMS, S3, RDS, SES). Those exist after 1E.3. Page stays mock at 1D close; real wiring lands in 1E observability scope.
Affects rows: C30 (moved to 1E section).
D-12 F-tier sidebar stubs — REMOVE, no "coming soon" placeholder
Empty entries that point at unbuilt features are speculation noise. No 404, no "coming soon" placeholder. The foundation sidebar contains foundation surfaces only; F-tier surfaces appear in the sidebar when their features ship.
Affects: every "Sidebar nav stub" row whose backend is F-tier (Clinic: 14, Console: 9, Portal: 8 = 31 entries — see Appendix). Each app's app-sidebar.tsx is pruned at 1D close.
Appendix — surface counts by app
Pre-decision (2026-05-07 audit):
| App | Total surfaces in 1D scope | Built | Partial / mock | Not started |
|---|---|---|---|---|
| 1D.1 Console | 36 | 5 (C7, C10, C28, C31, C32) | 6 (C8, C9, C11, C12, C13, C14, C30 mock-only) | 25 |
| 1D.2 Clinic admin | 22 | 1 (L20 legal docs) | 0 | 21 |
| 1D.3 Patient Portal | 12 | 6 (P1, P2, P3, P4, P5, P7) | 1 (P12 banner only) | 5 |
| 1D.5 Cross-org Account | 9 (incl. infra) | 0 | 0 | 9 |
| Total | 79 | 12 | 7 | 60 |
Post-decision (after D-1 / D-3 / D-6 / D-7 remove C19/C21/C24/C25/C33/C34, D-11 moves C30 to 1E):
| App | Total surfaces in 1D scope | Built | Partial / mock | Not started |
|---|---|---|---|---|
| 1D.1 Console | 29 (–7) | 5 (unchanged) | 5 (C30 moved out) | 19 (–6 removed) |
| 1D.2 Clinic admin | 22 (unchanged) | 1 | 0 | 21 |
| 1D.3 Patient Portal | 12 (unchanged) | 6 | 1 | 5 |
| 1D.5 Cross-org Account | 9 (unchanged) | 0 | 0 | 9 |
| Total | 72 | 12 | 6 | 54 |
Stub-only nav entries that are F-tier (out of 1D scope) — Clinic: 14, Console: 9, Portal: 8 = 31. Per D-12, all 31 are removed from the relevant app-sidebar.tsx at 1D close (no "coming soon" placeholder); they reappear in the sidebar when their backends ship.
If counted with the F-tier stubs: 121 distinct UI surfaces touched by this audit.
Appendix — domain handler → surface map (single-grep reverse-lookup)
| Handler | Routes count | Touched surfaces |
|---|---|---|
aimodels | 5 | C27 |
audit | 1 | C13, C28, L21 |
breakglass | 4 | C11, C29, L22 |
consents | 10 | C32, L13, P3, P4, P7 |
human | 3 | C1, C7 (login), C35, P5 |
impersonation | 5 | C11, L15, L16, P11, A8 |
integrations | 7 | L19 |
invites | 8 | L8, L9, L10, P3, P12 |
legaldocument | 9 | C31, L20, P9, P10 |
location | 5 | L5 |
organization | 13 | C7, C8, C9, C10, C14, L1, L3, L6 |
orgbilling | 2 | C12, L4 |
orgentitlements | 2 | C12 |
orgsettings | 2 | C14, L2, C33 |
patientprofiles | 2 | P6, A3 |
patients | 4 | C11, L11, L12 |
patientsubscriptions | 4 | C12, L14, P8 |
patienttiers | 4 | L17, C19 |
plancatalog | 4 | C18 |
platformproviders | 5 | C26 |
portalonboarding | 2 | P3 |
sharelinks | 5 | L10, P2 |
subscriptions | 8 | C12, C16, C17 |
webhooks | 8 | L18 |