LOKTORE v4.2.0 — Data · ER · API Utility+

Advanced utility v4.2.0 — tables · APIs · ER · dashboard · validation · code-gen · snapshots · Real API · Real Data Model · RBAC · Model Diff · ERD Diff · Functional Specification · Naming Convention — server-persisted (no browser storage)
connecting…
📈 Overview
📊 Data tables
🔗 ER diagram
🔌 APIs
🕸 API map
⚙ Generate
🧩 Real API
🗺 Real Data Model
🛡 RBAC
🆚 Model Diff
🧬 ERD Diff
📘 Functional Specification
🏗 Architecture v5
🏷 Naming Convention
Click a table node to inspect its columns, relationships and APIs.
Select an endpoint, or click + to add one.
Click an API to see endpoints it shares tables with
Click an API node to inspect the tables it touches and the endpoints related to it.
Select a real API on the left, import a JSON file, or click + to add one manually.
Click a table node to inspect its columns and relationships, or edit / delete it. Import a .sql file or build tables manually.

🛡 RBAC, Module Activation & Subscription Entitlements

Implementation roadmap for LOKTORE_RBAC_Subscription_Design_v4.7.docx — two independent engines (Entitlements + Authorization) orchestrated behind one canAccess() call. Visual flows, a phased delivery plan, and section-by-section documentation matching the source design doc.
🧭 Flowcharts & Diagrams
🗺 Implementation Roadmap
📚 Detailed Documentation

Role & Scope Hierarchy

§6 — 7 platform roles across the Platform → Country → Center → Site scope chain. A higher scope includes every lower scope within its boundary; configuration roles (amber) never touch business/medical data, operational roles (blue) never touch global configuration.

PLATFORMGlobal · every countryCOUNTRYOne country (e.g. Burkina Faso)CENTEROne health center + its sitesSITEOne physical site of a center↓ a higher scope includes every lower scope inside its boundarySUPER_ADMINMFA mandatoryGlobal governance, all countries. Composes/edits anyplan directly — no delegation needed.PATIENTNo MFASearches centers/professionals, books appointments,views own data (platform-scoped, but no admin rights).COUNTRY_ADMINMFA mandatoryManages entities within their own country only. Can begranted, and re-delegate, plan-edit rights within it.CENTER_ADMINFull management of a center and its organization. Candelegate operational actions to staff.HEALTHCARE_PROFESSIONALAttached to a country; can work across multiplecenters/sites of that country.SITE_ADMINOptional role. Manages local site operations.STAFFConfigurable delegated rights, scope =center/site/permissions. Country-scoped staff can holdConfiguration role — configures modules/rules/plans; no patient/business data accessOperational role — executes business operations; cannot modify global configuration🔒 MFA mandatory for SUPER_ADMIN and COUNTRY_ADMIN (§10.3)

Access Control Flow — canAccess()

§2 — Break-Glass is evaluated first (it must not sit behind the gates it exists to bypass), then Phase 1 Entitlements (commercial, cacheable, skipped for RBAC-only permissions), then Phase 2 Authorization (RBAC/ReBAC/ABAC). Every refusal carries a gate + reason.

canAccess(user, action, resource, context)STEP 0 · Break-Glass BypassActive break_glass_sessions row whose scopecovers this action/resource/context?AUTHORIZED — gate: break_glassno match ↓PHASE 1 · Entitlements (commercial)skipped entirely for RBAC-only / admin permissions1.candidate sources exist (own sub. OR funding link)?REFUSED · gate: entitlementreason: subscription_inactive2.frozen subscription_capability_access includes capability?REFUSED · gate: entitlementreason: not_in_plan3.frozen subscription_action_limits permits the action?REFUSED · gate: entitlementreason: action_excluded_by_plan4.capability operationally enabled (capability_activations)?REFUSED · gate: entitlementreason: capability_disabledALL required subscriber types must independently PASS (§1.4)all parties passed ↓PHASE 2 · Authorization (RBAC/ReBAC/ABAC)5.grant source: role permission OR valid delegation?REFUSED · gate: authorizationreason: no_grant_source6.deny override for this user + permission?REFUSED · gate: authorizationreason: permission_denied7.config role touching business/medical data?REFUSED · gate: authorizationreason: config_role_cannot_access_business_dataAUTHORIZED{ status, gate:null, reason, rulesEvaluated }

Access Catalog & Permission Resolution

§3.1, §3.3, §3.3.3 — resources × actions = permissions is a small, Platform-owned catalog owned by neither domain. capability_permissions (0..1) is what routes a request into commercial entitlement checking or straight past it for RBAC-only/administrative permissions.

resourcese.g. CLAIM, PRESCRIPTIONactionse.g. view, create, approvepermissionsresource + action pair×=ACCESS CATALOG — Platform-owned · owned by neither RBAC nor Entitlements (§3.1, §11)capability_permissions0..1 per permission (§3.3.3)mapped → entitlement-controlledfunctional_capabilitiesPhase 1 — Entitlements Servicechecks the subscriber's frozen planunmapped → RBAC-onlyRBAC-only permissione.g. PLAN+CREATE — Phase 1skipped, straight to Phase 2Phase 2 · Authorization Engine — role_permissions ∪ delegations, minus deny overrides(§2.2, §4.2) — every request passes through here regardless of the branch above

Subscription Lifecycle & Plan Versioning

§7.1, §3.7.1 — subscription states (trial/active/suspended/reactivated/cancelled/expired) plus the independent Draft → Review → Publish plan-version workflow that makes the frozen-snapshot guarantee enforceable.

TRIALtrial_ends_at setACTIVEfrozen snapshot liveSUSPENDEDnon-essential routes 403REACTIVATED→ back to ACTIVECANCELLEDterminalEXPIREDterminalpayment confirmednon-payment / adminreactivatedback to activeuser/admin cancelsnon-renewaltrial not convertedWhile suspended / expired / cancelled:all non-essential routes → 403 (gate: entitlement, reason: subscription_inactive).Profile + billing remain reachable. All data preserved — never deleted (§7.1, §3.10).Commercial operationsUpgrade — immediate re-freeze, instant access to new featuresDowngrade — re-freeze at cycle end, 30 days' notice; removed-module data frozen (subscription_frozen_data), restored automatically on re-upgradeRenewal — resync at renewal date, 30-day diff notice (§7.1)Plan Version Lifecycle (§3.7.1)DRAFTVIEW_PLAN / CREATE_PLAN_VERSIONREVIEW / DIFFEDIT_PLAN_VERSIONPUBLISHEDPUBLISH_PLAN_VERSIONARCHIVEDsuperseded by next publishprevious live version → ARCHIVED on publish; a draft never affects existing subscriptions until Publish(immutable once live — DB-enforced, §10.3). Granular permissions make prepare-vs-publish a real boundary, not a UI convention (§9.2).

Domain / Entity Cluster Overview

§11 — RBAC Core and Entitlements are structurally independent of each other; both depend only on the shared Access Catalog. Orchestration/Audit sits above both, the one legitimate place that needs visibility into every cluster at once.

RBAC COREroles · role_permissionsuser_roles · user_permission_overridesdelegations · break_glass_sessionsACCESS CATALOGresources · actions · permissionsPlatform-owned — owned by neitherENTITLEMENTS & SUBSCRIPTIONSfunctional_capabilities · plans/versionssubscriptions · capability_permissionssubscriber_types · funding rulesORCHESTRATION / AUDITaccessOrchestrator.canAccess() · authorization_decisions_logauthorization_decision_entitlement_sourcesreferencesreferences✕ no direct foreign key between RBAC Core and Entitlements (§11)reads identifiers from all three, for audit context only
0

Foundations & Governance Sign-off

Prerequisite — before any schema work

Lock the eight open decisions the document explicitly defers to governance/product/security (§12.3), since two of them (deployment topology, consistency SLA) shape every phase after this one.

  • Confirm deployment topology: Entitlements + Authorization as two modules inside one Governance service, not day-one microservices (§12.3 recommendation)
  • Sign off the bounded-staleness consistency model — Class C ≤5s, Class A/B ≤60s (§13.7) — a security/compliance decision, not engineering
  • Decide renewal resync policy (auto-resync + 30-day notice, per-plan override for sensitive tiers) and cascade-revocation behavior on admin permission removal
  • Resolve practitioner↔center attachment ownership and STAFF's authoritative membership source — gates PROFESSIONALS/STAFF capacity counting later (§3.8.7, §12.3)
§12.3§13.7
1

Access Catalog & Core Schema

Platform-owned foundation both RBAC and Entitlements depend on

Stand up the neutral resource layer first — resources, actions, permissions — since both domains reference it and it has zero dependency on either (§3.1, §11).

  • Create resources, actions, permissions tables with data_category on resources (business/medical vs. configuration)
  • Governance: Developer/System Administrator-only write access (MANAGE_ACCESS_CATALOG), never the functional Super Admin
  • Build the per-table constraint matrix (PK/FK/NULLABLE/UNIQUE/CHECK/DELETE POLICY/INDEX) the document itself names as the very next artifact after v4.7
  • ON DELETE RESTRICT everywhere — hard deletes rejected by the database; only is_active=false through the application
§3.1§3.1.1§3.1.2
2

RBAC Core — Roles, Delegation, Break-Glass

The Authorization Engine's own data

Implement roles, scoped assignment, deny overrides, operational delegation and the break-glass escape hatch — independently testable from Entitlements.

  • roles (scope_level, role_category) seeded with the 7 platform roles (§6): SUPER_ADMIN, COUNTRY_ADMIN, CENTER_ADMIN, SITE_ADMIN, HEALTHCARE_PROFESSIONAL, STAFF, PATIENT
  • role_permissions, user_roles (scoped country/center/site), user_permission_overrides (deny-only)
  • delegations table for operational delegation (§3.4, §9.1) — created→accepted→effective lifecycle, revocable, auto-revoked on detachment
  • break_glass_sessions with scope JSONB, token_hash, mandatory audit — and verify it is reachable for a center with a suspended subscription (§2.0's own regression test)
  • Partial unique indexes for every nullable-scope table (user_roles, user_permission_overrides, capability_activations) — §13.6
§3.2§3.4§3.12§9.1
3

Functional Capabilities & Module Composition

The commercial catalog, decoupled from navigation

Model what LOKTORE actually sells (capabilities) separately from how it's grouped for marketing (modules) and separately again from how it's clicked (menus).

  • functional_modules, functional_capabilities, module_capabilities (many-to-many, presentational only)
  • capability_permissions — the live-editable, governed exception inside frozen entitlement resolution (0..1 per permission, UNIQUE(permission_id)) (§3.3)
  • capability_activations — operational rollout gate by country/center_type/center, independent of the commercial plan (§1.1)
  • menus / submenus with navigation_permission_id (FK-plus-trigger integrity rule tying it to the submenu's own resource_id) (§4.3)
§3.3§3.3.1§3.3.3
4

Subscription Plans & Frozen-Snapshot Versioning

The core fix this revision exists to deliver

Build Draft → Review → Publish plan versioning so editing a plan never retroactively changes what an active subscriber can do — the document's stated core problem.

  • subscription_plans (polymorphic holder_type_id), plan_eligible_center_types, plan_versions (status: draft/live/archived)
  • plan_version_capabilities, plan_version_action_limits, plan_version_capacity_limits — all immutable once published, DB-enforced via trigger (§10.3)
  • Granular plan-edit permissions (VIEW_PLAN … PUBLISH_PLAN_VERSION, §9.2) wired through ordinary user_roles/role_permissions — no separate grant table needed
  • capacity_dimensions + capacity_dimension_subscriber_types (many-to-many, §3.8.7/v4.4) replacing the old max_users/max_sites columns entirely
§3.6§3.7§3.7.1§3.8.7
5

Subscriptions, N-Party Entitlements & Funding

The generalized, multi-party commercial core

Implement the polymorphic subscription + frozen snapshot, then the N-independent-entitlements algorithm that lets a capability require any number of subscriber types.

  • subscriber_types catalog with registered identity resolvers (Developer/System Admin governed, never Super Admin) — no hard-coded CENTER/PATIENT in code (§1.3)
  • subscriptions (polymorphic owner), subscription_capability_access, subscription_action_limits, subscription_history, subscription_frozen_data
  • subscriber_type_funding_rules, subscription_party_links, subscription_party_limits — sole source of truth for who can fund whom (§3.8.2–§3.8.4)
  • resolveActivationContext(), evaluatePartyEntitlement() / resolvePartyEntitlement() implementing the source-based (own subscription OR funding link), action-aware resolution of §3.8.5
  • subscription_trial_usage + extendTrial() to prevent cancel-and-resubscribe trial abuse, atomic first-trial check in createSubscription() (§3.8.8, §4.1)
§3.5§3.8§3.8.2–§3.8.8
6

Orchestrator — canAccess() & buildMenuTree()

The single public entry point everything else calls

Wire Break-Glass → Phase 1 → Phase 2 behind one function, then layer the tri-state navigation model on top for menu rendering.

  • accessOrchestrator.canAccess(user, action, resource, context) — break-glass bypass, then resource/action→permission→capability resolution, then the two phases in order (§4.3)
  • Regression-test the two defects this revision fixes by construction: a pure delegate (role-less, delegation-only) must be AUTHORIZED; break-glass must work against a suspended subscription
  • buildMenuTree() — ENABLED / LOCKED / HIDDEN tri-state navigation, HIDDEN entries never returned even with a marker; commercialAction (SUBSCRIBE/UPGRADE/RENEW/REACTIVATE) as a presentation-layer derivation only
  • getEffectivePermissions() = (role grants ∪ active delegations) − deny overrides, computed once, reused everywhere including navigation
§2§4.2§4.3
7

API Layer & Middleware

Two namespaces, one gateway

Expose the orchestrator and both services over HTTP, plus the internal integration point other services should call instead of reimplementing checks.

  • /api/v1/entitlements/* — plans, versions, publish/diff, subscriptions, upgrade/downgrade/renew, capability activations (§5.1)
  • /api/v1/rbac/* — menu/submenu, permission & role management, user RBAC, delegations, break-glass (§5.2)
  • POST /api/v1/access/check — the recommended internal integration point for canAccess(), replacing /internal/can (§5.3)
  • authorize() Express middleware constructing context.parties[] per the documented shape — never a stray top-level centerId (§4.4)
  • If this endpoint is ever consumed over a network boundary (not in-process): mTLS/service-token auth, client-side caching, circuit breaker, fail-closed policy — required from day one for any such consumer (§5.3)
§5§4.4
8

Audit, Security Hardening & Scalability

Make it correct under load, not just correct

Close the gap between a functionally-correct design and one that survives 5,000 req/s — the document's own scalability review (§13).

  • authorization_decisions_log + authorization_decision_entitlement_sources — async, batched, never synchronous on the request path; 100% of REFUSED + break-glass logged, 1–10% of AUTHORIZED sampled (§3.11, §13.5)
  • Staleness-classified caching: Class A (5–15min, policy data), Class B (1–5min, per-user assignments), Class C (seconds, security-critical negatives) (§13.2)
  • Materialized effective-permission/entitlement caches (rbac:eff:*, entitlements:eff:*, party_entitlement:*) with event-driven invalidation (§13.3)
  • PAP/PDP split inside the single Governance service boundary; fail-closed on an unverifiable policy snapshot (§13.4)
  • DB hardening: partial unique indexes, hot-path indexes (delegations, break_glass_sessions, subscriptions, capability_activations), read replicas, connection pooling (§13.6)
  • Load/chaos test targets: p99 ≤5ms cache-hit / ≤50ms cold, 5,000 sustained checks/sec, Redis-loss fail-closed, invalidation-storm without cache stampede (§13.8)
§13§3.11
9

Migration — Insurance Portal Pilot

Prove the resource layer earns its keep on a real build

The document's own recommended near-term path: build the Insurance-facing portal against the new model rather than attempting a full historical remap first.

  • For each legacy screen: submenu → identify/create its resources row → define its permissions (VIEW/APPROVE/REJECT-style) → map to the owning capability via capability_permissions → point the submenu at the resource
  • Seed an initial plan_versions v1 from each existing live plan so every current subscriber gets a frozen snapshot without disruption
  • Convert existing ON DELETE CASCADE foreign keys to ON DELETE RESTRICT without breaking existing data; decide dual-write vs. coordinated cutover
  • Re-point any existing Center/Provider screen already exposing a CLAIM-like object at the same resources row instead of duplicating it — the concrete test of whether the resource layer pays for itself
§13.9
Executive Summaryv4.7

LOKTORE controls two independent questions behind a single call, canAccess(user, action, resource, context): does the relevant party's subscription cover this feature (Entitlements), and is this specific user allowed to perform this action (Authorization/RBAC). The rest of the codebase never needs to know two systems exist.

Core problem this revision solves

In the original design, editing a commercial plan changed access immediately for every center already subscribed to it — contradicting LOKTORE's own commitment that pricing and features are locked until renewal. This revision introduces a frozen snapshot per subscription and full version history for plans (composition, price, limits), so a plan can be edited and published safely without touching any existing subscriber.

Commercial catalog structure

Built from functional_capabilities — the atomic, developer-owned features LOKTORE actually implements (teleconsultation, e-prescribing, claims tracking). Commercial modules (e.g. “Clinical Tools”) are a freely reconfigurable, Super-Admin-managed grouping with no technical logic of their own; plans reference capabilities directly, so reorganizing modules never affects a plan already sold. A Draft → Review → Publish workflow, backed by granular permissions, prevents accidental live edits.

Delegation

Administrative rights (Super Admin → Country Admin → Country Staff) follow standard RBAC, confined by one explicit rule: nobody can grant a permission or scope they do not themselves hold. Checked at grant time, not on every subsequent use (§9.2.2).

Two defects found and fixed by this revision's own engineering review
  • Delegated access was unreachable for any user without a direct role (fixed — grant source is now role OR delegation, §2.2)
  • Break-glass emergency access was unreachable for the exact scenario it exists for — a center with a suspended subscription (fixed — moved to an early bypass, §2.0)
Scalability finding

The engine as specified would not scale past a few hundred requests/second without caching. Remediation: data classified by staleness tolerance, materialized permission/entitlement lookups, asynchronous audit logging, and an explicit bounded-staleness consistency commitment requiring security sign-off (§13).

Multi-party entitlement model

Capabilities can require independent subscription from more than one kind of subscriber (a Center, a Patient, both, or more) — each side checked only against its own frozen subscription, never arbitrated against the other's plan version. Every subscriber type is a configurable catalog row (subscriber_types), never hard-coded (§1.3). Funding capability is derived from subscriber_type_funding_rules, never a flag.

Status

Ready for the SQL constraint matrix. Eight items remain for governance/product/security sign-off rather than engineering — see §12.3.

1. Overview & Architecture
1.1 Key Principles
  • Single Public Entry Point — all access checks go through canAccess(user, action, resource, context); no business service implements its own authorization or entitlement logic
  • Two Internal Engines, One Orchestrator — the Entitlements Service (commercial) and the Authorization Engine (Domain 10 — RBAC/ReBAC/ABAC); neither calls the other
  • Refusal by Default — anything not explicitly authorized is refused, at either gate
  • Entitlement Before Normal Authorization — after break-glass, Entitlements is checked before RBAC: it's a cheap, cacheable read against a frozen snapshot and gives a clearer refusal reason (“not in your plan” vs. “you lack the role”)
  • Entitlements Are Frozen at Subscription Time — a snapshot taken at subscription/renewal, never a live read of the editable catalog. Declared exception: capability_permissions is live-editable and governed (§3.3)
  • Capability Operational Activation Gate — independent of the commercial plan, a capability can be enabled/disabled per country, center type, or specific center (capability_activations)
  • Commercial Model ≠ Navigation Model — what's sold (modules/capabilities) and what's clicked (menus/submenus) are distinct hierarchies, linked only through the shared resources/permissions registry
  • Structured Decisions — every check returns { status, gate, reason, rulesEvaluated }
  • Rule Versioning, Zero-Trust, Separation of Concerns (configuration roles cannot access business/patient data)
1.2 What Changed vs. v1.0

v1.0 merged all 10 checks (subscription status, plan coverage, module activation, roles, overrides, delegation, break-glass) into a single can() function. This worked operationally but created two problems: no frozen snapshot (editing a plan retroactively changed access for active subscribers, contradicting LOKTORE's 30-days'-notice commitment), and coupled ownership (commercial + RBAC logic in one engine). v2.0+ keeps the single-entry-point ergonomics while splitting the internals into two independently ownable services and adding the snapshot mechanism.

1.3 Dynamic Subscriber Type Catalog

Every kind of subscriber — health center, patient, organization, insurer, or any future kind — is a row in subscriber_types, not a hard-coded value. The engine has no built-in concept of “CENTER” or “PATIENT.” Funding capability is derived, never a flag: whether a type can fund others is determined entirely by the existence of a row in subscriber_type_funding_rules. Creating/modifying a subscriber_types row is a structural decision reserved for Developer/System Administrator, never the functional Super Admin.

1.4 N Independent Entitlements, Never a Shared Policy

A capability can require entitlement from any number of independent subscriber types. None are arbitrated against a shared policy version — each party resolves independently against its own eligible sources, and ALL required types must independently pass:

transaction_allowed = AND over every required subscriber_type: resolvePartyEntitlement(partyType, partyId, capabilityId, permissionId, activationContext)

An imbalance between the number of parties of each required type is a normal commercial supply/demand signal, never a system inconsistency (§3.3.2).

2. Access Control Flow

canAccess() evaluates a break-glass check first, then two phases in order — see the Flowcharts tab for the visual version of this section.

2.0 Break-Glass Bypass (evaluated first)

Moved from Phase 2 step 9 to an early bypass branch — placed at the end, it was unreachable for its primary purpose (a suspended subscription would REFUSE at Phase 1 step 1 long before reaching it).

StepCheckIf matched
0Active break_glass_sessions row whose scope covers this action/resource/context?AUTHORIZED immediately, bypassing both phases, mandatory audit entry + any recorded constraints. No match → continue to Phase 1.
2.1 Phase 1 — Entitlements (N-party, source-based)

Algorithm rewritten around entitlement SOURCES (own subscription OR funding link), not the assumption that every party holds its own subscription — this correctly handles a Patient covered only through an NGO.

StepCheckIf Failed
1For each required party: candidate sources = own active subscription(s) + active subscription_party_links entriesREFUSED — reason: subscription_inactive, if zero candidate sources exist
2Discard sources whose frozen subscription_capability_access excludes the capabilityREFUSED — reason: not_in_plan, if none survive
3Among remaining sources, discard any whose frozen subscription_action_limits excludes the actionREFUSED — reason: action_excluded_by_plan, if none survive
4Is the capability operationally enabled for this country/center_type/center? (capability_activations)REFUSED — reason: capability_disabled
5Party PASSES if ≥1 source survived steps 1–3 and step 4 passedAUTHORIZED for this party — next party, or Phase 2 if last

A permission with NO row in capability_permissions (e.g. PLAN+CREATE, an RBAC-only/administrative permission) skips Phase 1 entirely — a deliberate branch based on the presence of a capability mapping, not on whether context carries a centerId.

2.2 Phase 2 — Authorization (Domain 10 Engine)

Fixes a v1.0 defect: role-permission failure used to terminate the pipeline before delegation (evaluated after it) was ever reached — a pure delegate was always refused. Fix: grant source is now the disjunction of role-permission and delegation.

StepCheckIf Failed / If matched
5Grant source: role grants this permission at matching scope, OR a valid/accepted/non-expired delegation?REFUSED — reason: no_grant_source
6Deny override for this user + permission? (checked regardless of role vs. delegation grant)REFUSED — reason: permission_denied — always wins over both
7Is a configuration role trying to access business/medical data? (role_category vs. resources.data_category)REFUSED — reason: config_role_cannot_access_business_data
8All gates passedAUTHORIZED
2.3 Signature & Response
canAccess(user, action, resource, context)
  • user — userId from Keycloak token
  • action — view, create, update, delete, approve, export, …
  • resource — a stable resources.code (e.g. PRESCRIPTION, CLAIM) — not a submenu code; the backend authorizes a business action, not a click on a screen
  • context — { countryId, siteId, parties: [ { type, id }, … ], targetId? } — a generic party collection, no fixed centerId/patientId fields

Response: { status: AUTHORIZED|REFUSED, gate: break_glass|entitlement|authorization|null, reason, missingSubscriberType?, rulesEvaluated: [...] }. The constraints field (trialEndsAt, maxUsers, …) is removed — canAccess() never evaluates capacity; that's a separate checkCapacity() call by the business service.

3. Database Schema

This is the section carried into LOKTORE Utility's live 118-table model (governance + entitlements domains, v3.7.1). The subsections below map 1:1 onto that implementation — cross-reference against the Data tables / ER diagram tabs for live column lists.

3.1 Access Catalog — resources, actions, permissions

A stable, UI-independent business object layer sitting between commercial capabilities and RBAC permissions, owned by neither domain (renamed acl_resources in this app to avoid colliding with the pre-existing scheduling.resources physical-resource table). Introduced so the same business object (a claim) can be secured and entitled once, then exposed identically through multiple portals' navigation.

3.2 Roles & Assignments

roles (scope_level, role_category), role_permissions, user_roles (scoped country/center/site), user_permission_overrides (deny-only). A configuration-category role cannot be assigned a permission on a business/medical-category resource — rejected at write time, not only at request time.

3.3 Functional Capabilities

functional_modules / functional_capabilities / module_capabilities are the commercial catalog. capability_permissions (capability ↔ permission, i.e. capability ↔ resource+action) replaces the earlier capability_resources — this is what disambiguates two capabilities sharing a resource, e.g. CLAIM_SUBMISSION (CENTER+INSURANCE) vs. CLAIM_REVIEW (INSURANCE alone). UNIQUE(permission_id): a permission can map to at most one capability.

3.3.1 Capability Subscriber Requirements

capability_subscriber_requirements declares which subscriber_types must independently pass entitlement for a given capability — generic, no special-cased type anywhere in the table or resolution engine.

3.3.2 Two-Sided Market

A capability can be funded by one subscriber type and consumed by another (e.g. an Insurer funding Patient teleconsultation without appearing in the Patient's own navigation) — an imbalance between funders and consumers is a commercial signal, not a data-integrity problem.

3.3.3 RBAC-Only Permissions

A permission with no capability_permissions row is RBAC-only / administrative (creating a plan, managing the Access Catalog, assigning a role) — no subscription entitles it, so Phase 1 is skipped entirely for it, always.

3.4 Delegations

Operational delegation (view / manage_availabilities / create_appointment / modify_appointment / cancel_appointment / configure_booking) — created (pending) → accepted → effective. Revocable any time, optional expiry, auto-revoked on detachment from center/site.

3.5 Subscriber Type Catalog

subscriber_types — every kind of subscription holder, wired to a registered identity resolver. Two-level governance: configuration is not no-code — adding a row makes the engine aware such a party may hold subscriptions, but the portal/roles/scopes/menus remain a separate engineering deliverable.

3.6 Subscription Plans

subscription_plans (polymorphic holder_type_id), plan_eligible_center_types. LOKTORE plans combine two independent axes: Depth (billing-only vs. full practice tools) and Visibility (invisible vs. patient-discoverable).

3.6.1 Plan Eligibility by Center Type / 3.6.2 Center Type Change / 3.6.3 Audience Lock-In

A plan can be restricted to specific center types; once a plan has ever had a subscription, its audience is locked in — a commercial mechanism kept distinct from the operational capability_activations gate.

3.7 Plan Versioning / 3.7.1 Draft → Review → Publish

plan_versions (status: draft/live/archived) with plan_version_capabilities / plan_version_action_limits / plan_version_capacity_limits, immutable once published (DB-enforced trigger). Granular permissions (§9.2) make prepare-vs-publish a real permission boundary, not a UI convention.

3.8 Subscriptions & Frozen Snapshot

3.8.1 Polymorphic Owner — subscriptions.owner_type_id/owner_id validated at write time via the identity_resolvers registry. 3.8.2 Funding Relations — subscriber_type_funding_rules is the sole source of truth for who can fund whom. 3.8.3 Coverage Links & Quotas — subscription_party_links + subscription_party_limits (isWithinLimit() NULL-safe helper). 3.8.4 Funding Rules. 3.8.5 resolvePartyEntitlement() / evaluatePartyEntitlement() — source-based, permission-precise resolution. 3.8.6 Action-Limit Freeze — subscription_action_limits. 3.8.7 Internal Capacity Limits — capacity_dimensions ↔ subscriber_types via capacity_dimension_subscriber_types (many-to-many, v4.4), replacing max_users/max_sites; PROFESSIONALS and STAFF are counted by their own resolver, never by counting RBAC role assignments. 3.8.8 Trial Abuse Prevention — subscription_trial_usage, UNIQUE(owner_type_id, owner_id, plan_id) as the actual arbiter, not an application check-then-insert.

3.9 Subscription History / 3.10 Frozen Data Retention

subscription_history (full version-aware change log) and subscription_frozen_data (data tied to removed modules on downgrade — preserved, restored automatically on re-upgrade, never deleted).

3.11 Authorization Rules & Decision Log

authorization_rules (versioned), authorization_decisions_log + authorization_decision_entitlement_sources (one row per source actually evaluated, plus grant_sources JSONB recording every ROLE/DELEGATION that granted). Freezes permission_id and capability_id at decision time — audit records are never reconstructed from the live Access Catalog. No synchronous write on the request path (§13.5).

3.12 Break-Glass Sessions

break_glass_sessions — token_hash, granted_by, scope JSONB, is_active, post_review_completed. Reclassified into a fourth Orchestration/Audit group alongside the decision log, since it legitimately needs visibility across RBAC and Entitlements for audit purposes.

4. Services
4.1 Entitlements Service

Owns subscriber_types, subscription_plans, plan_versions and its children, functional_capabilities, capability_permissions, capability_subscriber_requirements, subscriptions and everything under it, capacity_dimensions and its children. Does NOT own resources — shared, platform-owned, read-only from here. No dependency on the RBAC engine.

FunctionDescription
hasEntitlement(subscriptionId, resourceCode)Coarse aggregate check against the frozen snapshot — capability inclusion AND action-limit inclusion both required
getEffectiveEntitlements(subscriptionId)Full resolved capability → permission tree for a subscription, no submenu knowledge
evaluatePartyEntitlement(partyType, partyId, capabilityId, permissionId, activationContext)Single source of truth for the decision AND its reason — { status, reason }
resolveActivationContext(parties[], countryId)Shared helper producing { countryId, centerTypeId?, centerId? } — one resolution rule for both canAccess() and buildMenuTree()
resolvePartyEntitlement(...)Thin boolean wrapper around evaluatePartyEntitlement().status = PASS
createPlanVersion / setCapacityLimit / getCapacityLimit / checkCapacityPlan authoring and the capacity enforcement primitive (§3.8.7) — checkCapacity() must run inside the same atomic operation as the write it gates
extendTrial(subscriptionId, additionalDays, extendedBy, justification)Moves trial_ends_at forward; refused unless an active trial; immutable after creation
createSubscription(ownerTypeCode, ownerId, planId, versionId?)Generic replacement for per-type subscribe*() functions; resolves the subscriber_types row and its resolver; atomic first-trial check
linkParty / setPartyLimit / renewSubscription / downgradeSubscriptionFunding links, quotas, and lifecycle transitions — all owner-agnostic
4.2 Authorization Engine (Domain 10) — RBAC only
FunctionDescription
can(userId, action, resource, context)Phase 2 gates only: grant source (role OR delegation), deny override, config/business separation. Break-glass is not part of this function.
mayGrant(actorId, targetPermissionSet, targetScope)Confinement check (§9.2.1) — a distinct containment comparison, not a reuse of can()
getEffectivePermissions(userId, context)(role grants ∪ active delegations) minus deny overrides — deny subtracted at the source, every caller gets a deny-safe result
getUserRoles / getUserScopesAll role assignments / all scopes where the user has roles
4.3 Orchestrator — canAccess()

Implements §2 end to end: (0) break-glass; (1) resolve (resource, action) → permission, check for a capability_permissions row; (2) if mapped, resolve activationContext once and evaluate every required subscriber type via evaluatePartyEntitlement(); (3) call authorizationEngine.can(); (4) merge into the §2.3 response shape. A missing capability mapping is an expected branch straight to Phase 2 — never an error.

buildMenuTree(userId, navigationParty?, context) — tri-state navigation (ENABLED / LOCKED / HIDDEN, §4.3/v3.2.5+). LOCKED never grants access; a direct API call behind a locked screen is refused exactly like any unauthorized attempt. submenus.navigation_permission_id (DB-trigger-enforced to belong to the submenu's own resource) removes the old “any permission on the resource” ambiguity.

4.4 Middleware

Express authorize('delete','USER') factory constructs context per §2.3's shape — parties[] populated from whatever the request provides, never a bare top-level centerId — then calls accessOrchestrator.canAccess(); returns 403 with the structured { gate, reason } body if denied.

5. API Endpoints
5.1 /api/v1/entitlements/ (new namespace)
MethodPathDescription
POST/plansCreate plan (Super Admin, or delegated role)
POST/plans/:planId/versionsCreate a new draft version (Edit)
GET/plans/:planId/versionsList all historical versions
GET/plans/:planId/versions/:versionId/diffReview Changes — diff a draft against the current live version
PUT/plans/:planId/versions/:versionId/publishPublish — draft becomes live, previous live becomes archived
POST/modulesCreate/rename a commercial module (presentational)
PUT/modules/:moduleId/capabilitiesSet module_capabilities composition
GET/plans/compareCompare two plan versions
POST/subscriptionsCreate a subscription — { ownerTypeCode, ownerId, planId, versionId? }
GET/subscriptions/:subscriptionIdGet a subscription + resolved entitlements
GET/parties/:partyTypeCode/:partyId/subscriptionResolve a party's own active subscription by type + id
PUT/subscriptions/:subscriptionId/upgradeImmediate re-freeze
PUT/subscriptions/:subscriptionId/downgradeSchedules pending_plan_version_id/pending_effective_at
PUT/subscriptions/:subscriptionId/renewResync snapshot per plan policy
GET/subscriptions/:subscriptionId/historyFull version-aware change history
GET/checkhasEntitlement check (?subscriptionId, ?resourceCode)
GET / POST/capabilities/:capabilityId/activationsList / set operational activation
5.2 /api/v1/rbac/ (mostly unchanged from v1.0)

Menu/submenu management, permission & action management, role management, user RBAC, delegations, break-glass. Module/capability activation endpoints have moved out of this namespace entirely — activation now targets capability_id under /entitlements/capabilities/:capabilityId/activations, never module_id.

5.3 Orchestrated check
MethodPathDescription
POST/api/v1/access/checkcanAccess() for other internal services — the recommended internal integration point, replacing /internal/can

Inside the recommended modular monolith this is an in-process call. The moment any external service consumes it over the network: mTLS/service-token auth, client-side caching with event-driven invalidation, a circuit breaker, and a documented fail-closed policy (treat unreachable/erroring as REFUSED, never AUTHORIZED) are mandatory from day one.

6. Roles & Scope Hierarchy

Unchanged from v1.0 — see the Role & Scope Hierarchy diagram in the Flowcharts tab for the visual version.

RoleScopeCategoryDescription
SUPER_ADMINplatformconfigurationGlobal governance, all countries. Composes/edits any plan directly, no delegation needed. MFA mandatory.
COUNTRY_ADMINcountryconfigurationManages entities within their country only. Can be granted plan-edit rights and re-delegate within their country. MFA mandatory.
CENTER_ADMINcenteroperationalFull management of a center and its organization. Can delegate operational actions to staff.
SITE_ADMINsiteoperationalOptional role. Manages local site operations.
HEALTHCARE_PROFESSIONALcenteroperationalAttached to a country, can work across multiple centers/sites.
STAFFsiteoperationalConfigurable delegated rights, scope defined by center/site/permissions. Country-scoped staff can hold plan-edit rights.
PATIENTplatformoperationalSearches centers/professionals, books appointments, views own data. No MFA.

Scope hierarchy: Platform → Country → Center → Site. A higher-scope role includes access to all lower scopes within its boundary (a Burkina Faso Country Admin reaches every center/site in Burkina Faso, nothing in Côte d'Ivoire or Guinea). Configuration roles configure modules/rules/features/plans but cannot access patient/medical/business data; operational roles execute business operations but cannot modify global configuration — enforced via role_category.

7. Subscription Plan Model & Versioning

LOKTORE plans are not a linear tier ladder — two independent axes: Depth (“Billing only” vs. “Practice on Loktoré” — clinical tools, on-call, insurance) and Visibility (invisible to the public vs. discoverable by patients). Real modules: Organisation & digital front desk, Clinical tools, On-call & continuity of care, Insurance & reimbursement, Billing & financial tracking, Visibility & patient acquisition.

7.1 Lifecycle

States: active → suspended → reactivated → active · active → cancelled · active → expired · trial → active (after payment) · trial → expired.

  • Suspended/expired/cancelled: all non-essential routes return 403 (reason: subscription_inactive). Profile + billing remain reachable. All data preserved.
  • Upgrade — immediate re-freeze from the target plan_version; instant access to new features.
  • Downgrade — re-freeze at end of current billing cycle, 30 days' advance notice. Data tied to removed modules is frozen (subscription_frozen_data), restored automatically on a later upgrade.
  • Renewal — default: no change mid-cycle; 30 days before renewal the center is notified of the exact diff; snapshot resyncs at the renewal date. Individual plans can require explicit confirmation instead of silent auto-resync for sensitive tiers.
7.2 Illustrative Feature Matrix
FeatureBilling onlyAll-in-one (no visibility)Patient base mgmtAll-in-one
Organisation & front deskYESYESYESYES
Clinical toolsNOYESNOYES
On-call & continuityNOYESNOYES
Insurance & reimbursementNOYESNOYES
Billing & financial trackingYESYESNOYES
Visibility & acquisitionNONOYESYES
8. Hybrid Roles

A user may hold multiple cumulative RBAC roles simultaneously. RBAC role assignment does NOT determine STAFF/PROFESSIONAL business classification (§3.8.7). The physical Identity/account model for STAFF vs. PROFESSIONAL — one authentication account with separate business profiles, or genuinely separate accounts — is owned by the Identity domain, not decided by this document (v4.6 walked back an earlier “one account” phrasing that overreached).

Example: a practitioner who is CENTER_ADMIN at Center A and HEALTHCARE_PROFESSIONAL at Center B holds full admin capabilities at A and provider-only capabilities at B.

Same-Center hybrid roles

A healthcare professional may hold CENTER_ADMIN and/or SITE_ADMIN in the SAME Center they practise in — a clinic director who is both HEALTHCARE_PROFESSIONAL and CENTER_ADMIN for the same Center. These combinations never affect commercial capacity classification: the user remains counted under PROFESSIONALS, never additionally under STAFF, regardless of how many admin roles they also hold. WHAT a person IS commercially and WHAT a person CAN DO administratively are two independent axes — neither derives the other.

Audit record

There is no user-selected “acting role.” authorization_decisions_log records every grant source that actually granted the permission in grant_sources (every matching ROLE and any qualifying DELEGATION) — a prior “acting as” single-role phrasing was corrected as false whenever several roles independently grant the same permission at the same scope.

9. Delegation Systems

LOKTORE has two distinct delegation mechanisms. They are not interchangeable.

9.1 Operational Delegation (schedule/availability) — unchanged

A professional remains master of their schedule. Any third-party action requires explicit, granular, modifiable, revocable delegation (delegations table). Delegable actions: view, manage_availabilities, create_appointment, modify_appointment, cancel_appointment, configure_booking. Lifecycle: created (pending) → accepted → effective. Revocable any time, optional expiry, auto-revoked on detachment.

9.2 Plan-Edit Delegation — uses existing RBAC scoping, no new table

The right to act on a plan is not a fixed role and not a single permission — it is delegated, country-scoped, and follows a chain: Super Admin (always authorized, all countries) → LOKTORE Staff (granted, platform-scoped) → Country Admin (granted, scoped to their country_id) → Country Staff (granted by their Country Admin, same country_id).

PermissionTypical holder
VIEW_PLANCountry Staff and above
CREATE_PLANSuper Admin (any country_id incl. platform-wide), delegated Country Admin (own country only)
CREATE_PLAN_VERSIONCountry Staff and above — opens a draft
EDIT_PLAN_VERSIONCountry Staff and above — edits a draft
PUBLISH_PLAN_VERSIONTypically Country Admin and above only
ARCHIVE_PLAN_VERSIONTypically Country Admin and above only
ASSIGN_PLANAssign an existing plan to a center
VIEW_PLAN_HISTORYCountry Staff and above
MANAGE_MODULE / MANAGE_MODULE_COMPOSITIONCreate/rename/delete a module / edit its composition
MANAGE_CAPABILITY_ACTIVATIONEdit capability_activations
MANAGE_CAPABILITY_PERMISSIONSEdit capability_permissions — not granted by default
MANAGE_ACCESS_CATALOGCreate/edit resources, actions, permissions — Developer/System Admin only, never Super Admin
MANAGE_CAPACITY_DIMENSIONSCreate/edit capacity_dimensions — Developer/System Admin only
MANAGE_CAPABILITY_ENTITLEMENT_REQUIREMENTSEdit capability_subscriber_requirements — not granted by default
MANAGE_SUBSCRIBER_TYPESCreate/edit subscriber_types — Developer/System Admin only
MANAGE_FUNDING_RULESCreate/edit subscriber_type_funding_rules — Super Admin

This granularity makes Draft → Review → Publish a real permission boundary: a Country Staff can hold CREATE_PLAN_VERSION + EDIT_PLAN_VERSION without PUBLISH_PLAN_VERSION — they prepare, the Country Admin publishes. Each is an ordinary permission through user_roles/role_permissions — no separate grant table.

9.2.1 Confinement Rule

A user can never grant a permission or scope greater than what they themselves possess. Enforced at grant time via a dedicated mayGrant(actorId, targetPermissionSet, targetScope) function — not by re-checking the grantor on every subsequent use. Permission confinement: can only grant permissions currently held. Scope confinement: can only grant a scope contained within their own (a Burkina Faso Country Admin can never create a Côte d'Ivoire-scoped or platform-wide role).

9.2.2 Grant-Time vs. Use-Time Validity

Confinement is checked once, at grant time, deliberately NOT re-checked on every use. Plan-edit rights are ordinary RBAC assignments, not a dependent delegation chain — a Country Staff's granted permission becomes their own and does not depend on the Country Admin retaining it. Re-validating on every call would mean a manager leaving LOKTORE silently cascades to revoke dozens of legitimately onboarded staff — not a wanted property. What happens when an administrative permission is revoked is an open decision (§12.3).

10. Audit & Security
10.1 Audit Events
CategoryEvents
Menu/SubmenuMENU_CREATED, MENU_UPDATED, SUBMENU_CREATED, SUBMENU_UPDATED
RolesROLE_CREATED, ROLE_UPDATED, ROLE_DELETED
Role-PermissionROLE_PERMISSION_ASSIGNED, ROLE_PERMISSION_REMOVED
User-RoleUSER_ROLE_ASSIGNED, USER_ROLE_REMOVED, USER_ROLE_CASCADE_REVOKED
OverridesUSER_PERMISSION_OVERRIDE_ADDED, USER_PERMISSION_OVERRIDE_REMOVED
DelegationsDELEGATION_CREATED, DELEGATION_ACCEPTED, DELEGATION_REVOKED, DELEGATION_EXPIRED
Modules & CapabilitiesCAPABILITY_ACTIVATION_CHANGED, MODULE_COMPOSITION_CHANGED, CAPABILITY_ENTITLEMENT_REQUIREMENT_CHANGED, CAPABILITY_PERMISSION_CHANGED
ResourcesRESOURCE_CREATED, RESOURCE_MODIFIED (Developer/System Admin only)
Subscriber Types & FundingSUBSCRIBER_TYPE_CREATED, SUBSCRIBER_TYPE_MODIFIED, FUNDING_RULE_CHANGED
AuthorizationAUTHORIZATION_DENIED (security event, tagged with gate)
Break-GlassBREAK_GLASS_ACTIVATED, BREAK_GLASS_REVOKED, BREAK_GLASS_ACTION_PERFORMED
PlansPLAN_CREATED, PLAN_VERSION_DRAFT_CREATED, PLAN_VERSION_PUBLISHED, PLAN_VERSION_ARCHIVED, PLAN_ELIGIBLE_CENTER_TYPES_CHANGED
SubscriptionsSUBSCRIPTION_CREATED, SNAPSHOT_FROZEN, UPGRADED, DOWNGRADED, RENEWED, SUSPENDED, CANCELLED, REACTIVATED, FROZEN_DATA_RESTORED, PARTY_LINK_CREATED/REMOVED/EXPIRED, PARTY_LIMIT_CHANGED, CAPACITY_LIMIT_CHANGED, TRIAL_EXTENDED
10.2 Scoped Audit Log Access
RoleAudit Log Access
SUPER_ADMINGlobal access to all logs
COUNTRY_ADMINOnly logs relating to their country (incl. plan_versions they/delegates created)
CENTER_ADMINOnly logs relating to their center and attached sites
SITE_ADMINOnly logs relating to their site
Operational rolesOwn personal actions within functional scope
10.3 Security Requirements
  • Zero-Trust — every access verified, never implicitly granted
  • MFA mandatory for SUPER_ADMIN and COUNTRY_ADMIN
  • Refusal by default at either gate; configuration roles cannot access patient/medical data
  • All unauthorized access attempts logged as security events, tagged with the failing gate
  • Break-glass requires justification, time limit, full traceability
  • Audit logs are immutable, append-only, stored separately
  • Plan versions immutable once published — DB trigger rejects UPDATEs to plan_versions/plan_version_capabilities/plan_version_action_limits/plan_version_capacity_limits once status leaves draft
  • Periodic access review to detect/revoke unnecessary permissions and stale plan-edit delegations
  • Polymorphic subscription ownership validated at write time via the identity_resolvers registry, with a DB-level integrity check as defense-in-depth
11. Entity Relationship Overview

Restructured around four groups — see the Domain Cluster diagram in the Flowcharts tab. RBAC and Entitlements are structurally independent of EACH OTHER; neither references a table owned by the other. Both depend on a shared, Platform-owned Access Catalog (resources, actions, permissions) — the only tables either references outside its own boundary for its own operation.

  • RBAC cluster — roles, role_permissions, user_roles, user_permission_overrides, delegations, break_glass_sessions
  • Access Catalog — resources, actions, permissions — owned by neither domain
  • Entitlements cluster — functional_modules/capabilities, module_capabilities, capability_permissions, capability_subscriber_requirements, subscriber_types, subscription_plans and everything under it
  • Orchestration/Audit — authorization_decisions_log + authorization_decision_entitlement_sources — the accessOrchestrator's own record; legitimately references identifiers from every other group because auditing the orchestrator's decisions is precisely the one legitimate reason to need visibility into both sides at once

This is a stronger form of the “single intentional bridge” principle: rather than one domain reaching into the other's schema, both domains reach into neutral, stable ones — the Access Catalog for day-to-day operation, Orchestration/Audit for after-the-fact record-keeping.

12. Summary of Changes & Open Decisions
12.1 Kept as-is

The single canAccess() entry point, the two-engine split, refusal-by-default, zero-trust, and the role/scope hierarchy (§6) are unchanged since early revisions — this is a maturing, not a rewriting, of the original design.

12.2 Changed (headline items across the v2.0–v4.7 revision sequence)
  • Break-glass moved from a Phase-2 dead-end to an early bypass (§2.0) — it was unreachable for its own purpose
  • Delegation grant-source fixed to role OR delegation, not role alone (§2.2) — a pure delegate was always refused
  • capability_resources replaced by capability_permissions to disambiguate capabilities sharing a resource (§3.3)
  • Hard-coded CENTER/PATIENT replaced by a configurable subscriber_types catalog; the algorithm generalized to isPartyEntitledTo() over any number of parties (§1.3, §1.4)
  • max_users/max_sites removed entirely, replaced by the many-to-many capacity_dimensions model (§3.8.7, resolved v4.4)
  • Tri-state navigation (ENABLED/LOCKED/HIDDEN) replacing a binary shown/hidden model (§4.3)
  • authorization_decisions_log reclassified out of the RBAC cluster into a new Orchestration/Audit group, with a proper 0..N entitlement-sources table replacing a single nullable subscription_id FK (§11, §3.11)
12.3 Open decisions for CTO sign-off
  • Renewal resync policy — recommended: auto-resync + 30-day notice by default, per-plan override for sensitive tiers; large price-increase renewals may need forced confirmation regardless of default — confirm with commercial/legal
  • Deployment topology — recommended: Entitlements + Authorization as two modules inside one Governance service, not day-one microservices
  • Draft/Review/Publish — confirm no second-approver step is required at launch (current recommendation: no, granular permissions + audit suffice)
  • Cascade revocation of delegated admin permissions — recommended: keep existing grants valid by default, with a UI warning + explicit opt-in cascade at revocation time
  • Which business domain owns practitioner↔center attachment — confirmed NOT user_roles; a dedicated association/membership model must own it, gating both the auto-revoke-on-detachment contract and the PROFESSIONALS count_provider_key
  • Consistency SLA sign-off — Class C ≤5s / Class A/B ≤60s bounded staleness (§13.7) is a security/compliance decision, not engineering
  • [RESOLVED v4.4] max_users/max_sites vs. capacity_dimensions — removed entirely, folded into SITES/PROFESSIONALS/STAFF dimensions
  • STAFF definition scope for non-CENTER subscriber types, and the Identity/account model for STAFF vs. PROFESSIONAL — both new in v4.6, deliberately left open rather than answered by implication
12.4 Validation Criteria (representative sample — the full document lists ~25)
  • Reorganizing module_capabilities never changes the composition of any already-published plan_version
  • Publishing a new plan_version never modifies any subscription_capability_access row; only future subscriptions/renewals resolve to it
  • Confinement: a user attempting to grant a permission or scope they don't hold is REFUSED by can() at grant time — both permission and scope dimensions
  • A user holding a valid, accepted, non-expired delegation but no direct role grant is AUTHORIZED — the pure-delegate case previously always refused
  • A break-glass session correctly grants access to a resource in a center whose subscription is suspended, with gate: break_glass and a mandatory audit entry
  • A permission with no capability_permissions row is evaluated by Phase 2 only — Phase 1 skipped because there is no capability to resolve against
  • Two capability_activations rows at conflicting scope levels resolve by fixed priority (specific center > center_type+country > country > global), never ambiguously
  • No synchronous write to authorization_decisions_log on the canAccess() request path
13. Scalability Review & Operational Considerations

Answers one question: can the design in §1-§12 run strongly consistent and scale to thousands of requests/second? Short answer — it is strongly consistent today only because every check reads PostgreSQL directly, which is exactly why it will not scale as specified without the changes below. Scaling requires introducing controlled, bounded staleness, made explicit rather than implicit.

13.1 Problem Statement
  • A naive canAccess() performs 8–12 PostgreSQL queries per call
  • At 2,000–5,000 req/s with 1–3 checks per request → 20,000–150,000 queries/second against PostgreSQL for authorization alone
  • A synchronous INSERT into authorization_decisions_log on every decision is severe write amplification on top of that
  • A synchronous HTTP call to a central authorization service adds 5–20ms latency and a platform-wide single point of failure
13.2 Data Classification & Staleness Budget
ClassDataBudget
A — slow-moving policymenus, submenus, actions, permissions, role_permissions, plan_versions & mappings, module_capabilities, capability_permissions, resources5–15 min cache, event-driven invalidation
B — per-user assignmentsuser_roles, delegations, materialized effective-permission sets1–5 min cache, immediate invalidation on assignment events
C — security-critical negativesdeny overrides, break-glass revocation, subscription suspension, capability_activations offLive, or seconds with synchronous write-through invalidation
13.3 Effective Permission & Entitlement Materialization

Precompute effective permission sets per (user, scope) in Redis (rbac:eff:{userId}:{scopeHash}); entitlement gates resolved once per subscription_id and shared (entitlements:eff:{subscriptionId}). Deny overrides checked against a separate, always-fresh structure so a cached grant can never survive an explicit deny. Multi-funder resolution materialized as party_entitlement:{partyTypeCode}:{partyId}:{capabilityId} → { entitled, allowedPermissionIds }.

13.4 Deployment Model — PAP / PDP

Refines, doesn't replace, the §12.3 one-service recommendation: the Governance service is the Policy Administration Point (source of truth, only writer); canAccess() reads a locally replicated policy snapshot (in-process cache backed by Redis) as the Policy Decision Point — same service boundary, not a network hop. Fail-closed: an unverifiable policy snapshot (cache empty AND PAP store unreachable) → reject, never authorize.

13.5 Asynchronous Decision Logging

Decisions published to a queue, batch-inserted — never synchronous on the request path. 100% of REFUSED + 100% of break-glass logged; AUTHORIZED sampled 1–10% (raisable during an investigation). Partitioned by month; UPDATE/DELETE revoked from the application's DB role.

13.6 Database Hardening for Scale
  • Partial unique index on subscriptions: UNIQUE(owner_type_id, owner_id) WHERE status IN ('active','trial')
  • UNIQUE NULLS NOT DISTINCT on every nullable-scope table (user_roles, user_permission_overrides, capability_activations, …)
  • Hot-path indexes: delegations(delegate_user_id, is_active, expires_at), break_glass_sessions(user_id, is_active, expires_at), subscriptions(status, expires_at), capability_activations(capability_id, country_id, center_type_id, center_id)
  • Read replicas for getEffectivePermissions/getEffectiveEntitlements/buildMenuTree/reporting; connection pooling (PgBouncer)
  • All expiries evaluated live via expires_at > now(); sweeper jobs only reconcile flags for listing/reporting performance
13.7 Consistency Model

Target: bounded staleness with event-driven invalidation, not global strong consistency. Class C ≤5s, Class A/B ≤60s. Must be reviewed and signed off by security/compliance — it changes what “Zero-Trust — every access is verified” means in practice (§1.1).

13.8 Validation & Capacity Targets

Load: p99 ≤5ms on cache hit, ≤50ms cold path, 5,000 sustained checks/sec with PostgreSQL CPU <50%. Chaos: Redis loss (fail-closed + recovery), PAP outage (PDP survives on last-known-good snapshot within budget), invalidation storm without cache stampede (request coalescing/singleflight). Correctness: pure-delegate authorized, break-glass on suspended subscription, revocation-to-effect latency measured per class against the §13.7 SLA.

13.9 Migration Strategy

A full v1.0→current schema migration (frozen plan_versions, functional_capabilities, resources/permissions replacing submenu-keyed security) is non-trivial and out of scope for this document. Concrete near-term path: the Insurance portal, built largely from scratch — legacy submenu → resources row → permissions → capability_permissions mapping → submenu.resource_id. Existing Center/Provider screens exposing the same object are re-pointed at the same resources row rather than duplicating it (§13.9 — see Roadmap Phase 9).

13.10 Storage Growth

Partitioned, sampled decision logging (§13.5) and materialized caches (§13.3) are the primary levers keeping storage and write volume bounded as request volume scales — governed by the same staleness budget and retention-per-country policy defined above.

🆚 Data Model Comparison — v3.6 vs v3.7.22

Table-by-table diff of the app's own data model, computed directly from each version's embedded model (not from prose notes). Every added, removed, and modified table below is exact — column-level, not summarized.

🧬 ERD Comparison — LOKTORE_ERD_v4.7.html vs v3.6

Structural diff between the standalone ERD v4.7 reference document (parsed directly from its Mermaid source — entities, columns, PK/FK/UK) and LOKTORE-Studio-v3.6's own ER diagram. Two entities collide by name with an unrelated v3.6 concept; those are called out explicitly below.

Relationship summary

Relationships that bridge an ERD v4.7 entity to an existing v3.6 table

🏗 Architecture v5

Rebuilt target design (phases A→E): services, use cases, states, ownership, dependencies, ADR. Read-only; the v5 data model and API catalog are editable in their own tabs.
🏛 System Overview
🧩 Services
🕸 Service Dependencies
👥 Actors
🎯 Use Cases
📐 Invariants
🔁 State Machines
🗄 Data Ownership
🔀 Existing → Target
📨 Event Catalog
Deployment
🧭 Traceability
📝 ADR
Open Questions
Consistency Checks

📘 Functional Specification

LOKTORE_Consolidated_Functional_Specification_EN_Updated.docx — parsed & structured (V1.08, 15 Parts + Annex).
🗺 Graphical Representation
🚧 Implementation Roadmap
📖 Detailed Documentation

🗺 Graphical Representation

Six diagrams summarizing the functional specification: governance & delegation, the entity model, the central authorization engine's locking chain, Provider/Association lifecycle, entity life-cycle & visibility, and the two income models with multi-country pricing controls.

Governance & Delegation Hierarchy

Front matter, §24.1-24.2, §27.7 — three administrative levels. The Super Admin sets the functional ceiling (never delegated); the Country Admin freely composes its country's offer under that ceiling; the Center/Site Admin operates the entity day-to-day. “We never block the Country Admin a priori. We report it.”

SUPER ADMIN PLATFORMGlobal governance · every country · sets the functional ceilingCOUNTRY ADMINOne country · composes the offer under the ceilingCENTER / SITE ADMINOne Center (+ its Sites) · operational managementDefines the FUNCTIONAL CEILING — never delegated• Registers modules, types of Center, types of Provider• Sets country / Center-type scope for each module• Global activation / deactivation of a module• Country quotas, charging points, alert thresholds• Lifts locks it has itself placedComposes the OFFER — full autonomy under the ceiling• Creates & prices subscription plans for its country (V1.07)• Assigns plans, creates promotions, sets overrides• Manages country's Organizations & Insurance affiliations• Can lock a national resource against lower levels• Never blocked a priori — reported via notification + auditOperates the ENTITY — day-to-day business execution• Creates/manages Sites, Providers, staff• Manages calendars, availability, appointments• Approves / rejects membership applications• Delegates to Site managers & staff within its scope"We never block the Country Admin a priori. We report it."Threshold exceeded → notification + audit, never an interruption. Proven drift → locking (§26.7).

Structure of Entities & Relations

“Structure of entities-relations” overview, Parts II-V — Country isolates data and carries brands; Center/Site/Provider/Association form the operational core; Reservation, Plan, Organization and Insurance attach to it; Locking can suspend any of them from above.

COUNTRYisolates databears 1+ MARKSCENTERbelongs to COUNTRY1+ SITESSITEcalendars, resourcesavailabilityPATIENTactive-country searchmakes RESERVATIONsPROVIDERindependent profileconsents to linksASSOCIATIONProvider ↔ Center/Siteconditions visibilityRESERVATIONProvider / Resourcestatus, price, paymentPLANCenter or Patientmodule access matrixORGANIZATIONsingle countrygrants modulesINSURANCEaffiliated to Centers3rd-party payerLOCKINGby admin levelsuspends resourcemay createconsentsenablesbooksactive countrysuspendssuspendsEvery access decision on these entities is resolved by the single central authorization engine (see diagram3) and traced in the audit log.Cross-cutting relationships not drawn above (to avoid crossing lines):• INSURANCE is affiliated with one or more CENTERs for third-party-payer processing and teletransmission (§30.5).• PLAN attaches to a CENTER or a PATIENT and carries its module access matrix (§36-37).• LOCKING can also suspend a CENTER (entity lock) or freeze a PLAN (plan lock) — see diagram 3 for the full 7-scope chain (§26.7.5).• ORGANIZATION is linked to a single COUNTRY and grants modules to PATIENT members of that same country only (§36.5).ASSOCIATION — the Provider↔Center/Site consent link that conditions calendars & visibilityLOCKING — administrative takeover; suspends a resource for every actor who depends on it, never destroysdata

Central Authorization Engine & Locking Chain

§26.3-26.7.10, Annex A.1 — every access decision, whatever the actor or resource, is resolved by one engine, in one fixed order, stopping at the first rule that applies. No business module implements its own permission logic.

can(user, action,resource, context)1Global module deactivation?REFUSED — no exceptions possible2Unavailable in country / Center type?REFUSED — functional ceiling, never delegated3Entity / plan / module / quota / affiliation /pricing / catalog LOCK by a higher level?REFUSED — “locked by [level]”4Organization rights (patients only)adjusts effective right5Subscription plan — access matrixadjusts effective right6Role & scope of the actoradjusts effective right7Default refusalREFUSED if nothing explicitly grantedEvaluated at a single point:the central engine (§26.3).No business moduleimplements its own logic.Decision returns:status · reason · rulesevaluated · constraints.

Provider Creation Modes & Association Lifecycle

§15-18 (Provider), §19-21 (Association) — four entry modes converge on one independent, single-country Provider profile; associations move through application/invitation → consent → active → suspended/withdrawn, gating calendars and visibility.

4 entry modes create one independent Provider profileSelf-registrationProvider signs up directlyCreated by a CenterCenter onboards the ProviderCreated by a SiteSite onboards the ProviderCreated by Super AdminPlatform seeding / onboardingPROVIDER PROFILEindependent · single country · globalAssociation lifecycle — a granted, revocable consent link to a Center / SiteApplication /InvitationConsent given(Provider accepts)ACTIVEassociationSuspended /WithdrawnEffect of association state: calendars & public visibility are gated by ACTIVE associations only(§19-21). A locked Center suspends the association's effects, never the Provider's independentprofile (§26.7.11).

Entity Life Cycle, Activation & Patient Visibility

§22-23 — life cycle (draft → complete → published) and activation status are independent axes; automatic publication requires no manual step, and Patient visibility requires both axes plus an active association and no lock.

Axis 1 — LIFE CYCLE (progress through completeness)DraftCompletePublishedAutomatic publication once completeness & consistency conditions are met (§22) — no manual step required.Axis 2 — ACTIVATION STATUS (independent, immediate)ACTIVEentity produces effectsINACTIVE / LOCKEDimmediately suspended, life-cycle unchangedPatient visibility requires BOTH axes to alignPublishedActive + unlockedVISIBLE to Patientwith active association (§23)Any single condition failing (draft, unpublished, inactive, or locked at any admin level) hidesthe entity from Patient search and booking — the conditions are evaluated together, not asalternatives.

Two Income Models & Multi-Country Pricing

Front matter “Two income models”, §36-38 — flat-rate subscriptions and booking commissions are cumulative and never touch platform funds; country-scoped pricing is bounded by Super-Admin charging points and can be frozen by pricing/catalog locks.

Two cumulative revenue streams — platform never touches the money flowFLAT-RATE SUBSCRIPTIONPatient, Center or Organizationpriced country-by-country, in local currencyBOOKING COMMISSIONDue by Center per honoured appointmentinvoiced a posteriori — Patient pays Provider directlyCountry-scoped pricing engine (§38)Country currency& price pointsCountry Adminsets price / promosCharging points(Super Admin ceiling)Pricing lock /catalog lockWarning thresholds (overrides, promotions, plan changes, rate changes) never block — theynotify the Super Admin and feed the audit log (§27.8.7-9). Proven drift is handled by locking,not by prior approval.effective quota = max(plan quota, override quota) — an override can neverturn a later plan upgrade into a ceiling (§37.7.3)

🚧 Implementation Roadmap

Ten phases derived from the document's own Part ordering (I → XV) plus the explicit priority signals in Annex A.6 ("Items to be decided") and the MVP / deferred tags scattered through the specification. Each phase lists its goal, concrete deliverables, dependent phases, an estimated timeline and a priority level.
0

Phase 0: Governance Foundations & Functional Ceiling Registry

Prerequisite — every later phase reads this registry
⏱ Weeks 1-2⬤ Critical priority

Stand up the three-level administrative model and the functional-ceiling registry before any business entity exists, since the doctrine — “the Super Admin sets the ceiling, the Country Admin composes under it” — gates everything downstream (front matter, §24.1, §27.7.2).

  • Holding / multi-tenant isolation model — one holder's data never visible to another
  • Module, type-of-Center and type-of-Provider registries (Super-Admin-only, never delegated)
  • Country quotas, charging points and alert-threshold registries (§27.7.2, §27.8.2, §27.8.7)
  • Skeleton can(user, action, resource, context) engine with hard default-refusal (§26.5)
Depends on: none
Front matter§24.1§27.7.2
1

Phase 1: Country, Brand, Location & Center/Site Core

The root entities every other module attaches to
⏱ Weeks 2-5⬤ Critical priority

Build multi-country isolation, white-label branding, geolocation and the Center/Site hierarchy — the structural backbone described as the transverse framework before any concrete entity (Parts II-III).

  • Country-scoped data isolation and active-country resolution (§4)
  • Multi-brand / white-label configuration with priority resolution rule (§5, §6.2.5)
  • Location & geolocation, including non-registered / passive-SEO Centers (§6, §14)
  • Feature configuration by country and Center type — the functional-ceiling application layer (§7)
  • Center identification/activation status, Center types, establishment, default Site (§8-13)
Depends on: Phase 0
§4§5§6§7Part III
2

Phase 2: Health Providers, Patient File & Associations

Independent Provider profile plus the consent-based link to Centers
⏱ Weeks 5-8⬤ High priority

Implement the four Provider-creation modes and the Association lifecycle that gates calendars and visibility, plus the clinical-practice module referenced immediately after (Part IV, IVa, V).

  • Provider types & the four creation modes: self-register, by Center, by Site, by Super Admin (§15-16)
  • Independence of the Provider profile; case of a created-then-independent Provider (§17-18)
  • Clinical practice: patient file, consultation and prescriptions (§18 bis)
  • Association model — applications, invitations, suspension & withdrawal (§19-21)
Depends on: Phase 1
§15§16§18 bis§19-21
3

Phase 3: Life Cycle, Visibility & Central Authorization Engine

The single decision point every other module calls — largest and most interdependent phase
⏱ Weeks 8-13⬤ Critical priority

Deliver the full central engine described in Annex A.1 as the unified resolution chain for every right in the system, together with the entity life-cycle/visibility rules and the 3-level delegation & hierarchical-locking model (§22-27, Annex A.1).

  • Entity life cycle (draft → complete → published) as an axis independent of activation status (§22-23)
  • Key & hybrid roles: Super Admin, Country Admin, Center/Site Admin, Provider, Staff, Patient, third-party-org roles (§24-25)
  • Central decision engine can(user, action, resource, context) with structured AUTHORIZED/DENIED decisions (§26.1-26.6)
  • Delegation chain + hierarchical locking across the seven lock scopes, evaluated in the fixed order of §26.7.10
  • Extended Super Admin capacity: Center/Provider seeding, non-interference principle (§27.1-27.6)
  • Country-Admin plan-creation delegation, its monitored-economic-gesture regime and catalog-lock recapture (§27.7)
Depends on: Phase 0, Phase 1, Phase 2
§26§26.7§27Annex A.1
4

Phase 4: Availability, Reservations & Medical References

The platform's declared “functional core”
⏱ Weeks 13-17⬤ High priority

Build the calendar, appointment and overbooking engine the document itself calls the functional core, together with the medical acts/reasons catalogue it depends on for booking context (Part VIII-IX).

  • Availability & appointment module — modes, attribution, pricing, overbooking, resources (§28)
  • Mixed appointments (Provider calendar + resource) and niche overbooking stock handling
  • Global act-type catalogue (label, synonyms, external codes) vs. local Center/Site reasons (§29)
  • Operational review management for completed appointments (§29 bis)
Depends on: Phase 2, Phase 3
§28§29
5

Phase 5: Insurance, Third-Party Payers & Notifications

Flagged by the document itself as a model under consolidation — build to spec, expect rework
⏱ Weeks 17-20⬤ Medium priority

Implement the interim Insurance/third-party-payer model and the multi-channel notification system, while explicitly carrying forward the document's own caveat that Insurance (§30) is “developed separately and intended to be migrated later” (Annex A.6: High — structural).

  • Insurance, third-party payer and partner-Center management (§30) — build against documented interim model
  • Insurance-Center affiliation, teletransmission, affiliation locking (§30.5, §30.12)
  • Multi-channel notification system with priority ordering and per-actor content (§35)
  • Flag for re-architecture once the dedicated Insurance specification is finalized and frozen (Annex A.6 §30)
Depends on: Phase 3, Phase 4
§30§35Annex A.6
6

Phase 6: Subscriptions, Access Control & Multi-Country Pricing

The single largest part of the document — the commercial core
⏱ Weeks 20-27⬤ Critical priority

Deliver Patient, Center and Organization subscription/access control together with multi-country pricing, currency and billing — Part XIII alone spans roughly a third of the entire specification (§36-39).

  • Patient subscriptions & access control: plans, overrides, organization-sourced rights (§36)
  • Center subscriptions & access control: plan/module access matrix, quotas, override priority-by-level (§37)
  • Multi-country pricing, currency, charging points, promotions, pricing lock, billing mode during locking (§38)
  • Organization subscriptions & access control, branch/head-office structure (§39)
  • Country-scoped plan catalogue with catalog-lock recapture, alert-threshold-monitored plan creation (§27.7.7)
Depends on: Phase 3
§36§37§38§39
7

Phase 7: Security, Audit & Compliance Hardening

Can run alongside Phase 6 — cross-cuts every module already built
⏱ Weeks 20-24 (parallel to Phase 6)⬤ Critical priority

Close out the platform's non-functional guarantees: full audit traceability, break-glass hardening, data security/compliance and the explicit non-functional requirements (Part XI).

  • Audit, traceability and logging for every privileged action, lock, and override (§31)
  • Break-glass mechanism hardening: time-limited, justified, fully traced, auto-revoked (§24.1.3)
  • Data security & compliance: zero-trust, encryption, isolation guarantees (§32)
  • Non-functional requirements sign-off: performance, availability, regulatory compliance (§33)
Depends on: Phase 3
§31§32§33
8

Phase 8: Portal Consolidation — Provider, Center & Platform

Wires the completed backend into the three user-facing surfaces
⏱ Weeks 27-31⬤ Medium priority

Assemble the three portals against the by-then-complete capability set, following the document's own consolidated-coverage cross-reference of every prior Part (Part XIV-XV).

  • Provider portal — consolidated functional coverage (§40)
  • Center portal — consolidated functional coverage + extensions (§41, §44)
  • Platform / Super Admin portal — country administration + extensions (§42, §45)
  • Common principles shared across all three portals (§43)
Depends on: Phase 4, Phase 5, Phase 6, Phase 7
Part XIVPart XV
9

Phase 9: Deferred Items & Post-MVP Arbitration

What is deliberately NOT in MVP scope, per the document's own open-items register
⏱ Ongoing / post-launch⬤ Deferred priority

Track the items Annex A.6 explicitly defers to product/legal arbitration rather than treating them as omissions — building against them now would be premature given they are marked unresolved in the source document itself.

  • Insurance model migration & scope freeze — High/structural, blocks §30 finalization (Annex A.6.1)
  • Third-party-paying portal & claims review — High, “not to be implemented before arbitration of the Insurance Model” (§39.4.1)
  • “Admin insurance” role definition — Medium, blocks §35.4.8 notification implementation
  • SMS quota/rebilling strategy — deliberately deferred “to be defined at maturity” (§35.9.8)
  • Organization withdrawal plan at J+45 — Medium, blocks the unpaid-Organization cycle (§39.7, §38.10.2)
  • Legal qualification of store vs. web channel coexistence under Apple/Google policy (§38.9)
  • Cross-country compliance regime if transversal nominative access is ever opened (§24.7.4)
Depends on: Phase 6
Annex A.6

📖 Detailed Documentation

Full section-by-section reading view — every Part, numbered section and sub-section from the source document, rendered verbatim (including all 117 tables) from the parsed document tree. Search or browse the index on the left; the reading pane on the right shows the complete text of the selected item.

🏷 Naming Convention

API naming convention v1.0 & Domain naming register v1.0 — the engineering standard this catalog follows, and who owns what.
📐 Convention Rules
🗂 Domain Register
🧮 Identifier Derivation

📐 API Naming Convention v1.0

Anatomy of a path: /api/v{major}/{resource}[/{id}][/{sub-resource}][/{id}][/{action}] — codifies the majority form the catalogue already follows, resolves the places it contradicted itself, gives one decision rule per question.
What changed in this catalog — v4.2.0applied

This tab documents the standard; the panel below is what actually changed in this catalog to comply with it.

CategoryCount
API paths renamed19
New endpoints added2 (GET /professionals/me, GET /jobs/{jobId})
Endpoints converted to the async job-resource pattern5
Event names corrected7 distinct renames (4 entity.* → 12 per-aggregate events)
Permission actions folded away14 (73 → 59 distinct actions)
State machines corrected8 reviewed, 5 with structural fixes (appointment, subscription, verification, unregistered center, invitation)
Column-level comments added1997 / 2270 columns (88.0%)

Full rename table, event/permission fold rationale, and state-machine citations are in the Architecture v5 tab (Open Questions & ADR panels) and in the API catalog itself — every renamed/converted endpoint carries a note explaining the change.

How to read this tab

Two source documents are reproduced here as living reference: the API naming convention v1.0 (the rules) and the Domain naming register v1.0 (who owns which resource, and where two domains currently reach into the same root). Where the register says "the catalogue currently does X", read that as the state before v4.2.0 — the fixes panel above says what changed. Where the two source documents disagree with each other (see §16 below), that's flagged, not silently resolved.

01–02 · Foundations & casing

One sentence to remember: the URL is kebab-case, the wire is camelCase, the database is snake_case. Translation happens at the persistence boundary and never leaks upward.

LayerConventionExample
Path segmentkebab-case, lowercase/act-configurations
Path parameter{camelCase} in braces{localActTypeId}
Query parametercamelCase?pageSize=50
JSON fieldcamelCaserescheduledFromId
Enum valueSCREAMING_SNAKEBY_PROVIDER
Error codeSCREAMING_SNAKEVERSION_MISMATCH
HeaderX-Kebab-CaseX-Idempotency-Key
Table & columnsnake_case, plural tableappointment_status_history
Domain eventaggregate.pastTenseappointment.confirmed
PermissionRESOURCE+ACTIONAPPOINTMENT+DECIDE
Servicekebab-casecenter-provider

Identifier language is US English. The product ships in French; the API does not — a francophone UI is a translation layer applied at the edge, never a naming input.

03–04 · Structure: resources, nesting, audience
/api/v{major}/{resource}[/{id}][/{sub-resource}][/{id}][/{action}]

Resources are plural nouns, never a verb, never singular. Two exceptions: singletons (a child that can only ever have one instance takes the singular — /centers/{centerId}/subscription) and uncountable nouns (/staff, /pricing, /audit).

Nest one level, then flatten

Collections hang off their parent; items are addressed by their own global ID.

GET /centers/{centerId}/sites (scoped list + create) — ✓ GET /sites/{siteId} (the item itself) — ✗ GET /centers/{centerId}/sites/{siteId} (parent adds nothing once the child has its own global id).

Exception — composite keys: when the relation has no ID of its own and genuinely needs every parent segment to identify a row, deep nesting is correct: DELETE /centers/{centerId}/professionals/{professionalId}/site-assignments/{siteId} — three parameters, legitimate, because the assignment row has no ID of its own.

Parameters carry their entity's name — write {invoiceId}, never a bare {id}.

Never put the audience in the path

/admin/brands and /brands are the same resource seen through different permissions — authorization belongs in the token and the policy engine, not the URL. One carve-out: /public/* is legitimate for genuinely unauthenticated, cacheable, SEO-facing reads (a real infrastructure difference: CDN in front, no auth middleware, different rate limits).

05–07 · Operations: verbs, reserved suffixes, jobs

Standard CRUD carries no verb (the HTTP method is the verb). Everything else is POST + an imperative verb: ✓ POST /appointments/{id}/cancel — ✗ /cancellation (noun) — ✗ /do-cancel (filler).

The four decision verbs (§05) — assign one job each, don't mix them
VerbWho actsBody carries
decideAn authority rules on a pending request{ decision, reason }
respondThe subject accepts or declines something offered to them{ response }
transitionA state machine moves; target state is explicit{ toState }
resolveA dispute or conflict is closed out{ outcome, note }
Reserved verb registry (§06) — one fixed meaning each, across the whole API
SuffixMeaningReturns
/resolveWalk a config hierarchy, return the effective valuethe resolved object
/checkEvaluate a policy, no side effect{ allowed, reason }
/previewDry run — what would happen if I sent thisthe projected result
/simulateRun a rules engine against sample inputevaluation trace
/duplicateDeep-copy the resource201 + the new resource
/upload-urlMint a pre-signed PUT{ url, fields, expiresAt }
/download-urlMint a pre-signed GET{ url, expiresAt }
/exportStart an async bulk read202 + job handle
/importStart an async bulk write202 + job handle
/bulkBatched write over many items207 multi-status

⚠ Known nuance, not fixed here: /resolve has two meanings across §05 (decision verb, POST) and §06 (reserved verb, GET) — both already coexist in this catalog (e.g. POST /disputes/{id}/resolve vs. GET /pricing/resolve). In practice, HTTP method + resource context disambiguate cleanly. Flagged as feedback for the convention document's own next revision (see open question OQ-N2).

Long work becomes a job resource (§07)

Anything that can exceed a request timeout returns 202 immediately with a first-class, pollable resource:

POST /commission-statements/generate → 202 · Location: /jobs/{jobId} · { jobId, status: "QUEUED", pollAfterMs }
GET /jobs/{jobId} → { status: "RUNNING|SUCCEEDED|FAILED", progress, result, error }

One /jobs namespace for the whole platform, not one per service — applied in v4.2.0 to 5 endpoints (commission-statement generation, audit export, 3 bulk imports). See ADR-N2 for how ownership is resolved without a shared table or a new microservice.

08–09 · Cleanup & polymorphic patterns
One name per concept (§08)
KeepRetireWhy
/reference/{referentialKind}/config/center-types, /config/professional-types, /config/countriesGeneric form already handles every kind
/discovery/centers/{slug}/profiles/centers/{slug}"discovery" names the actual use case
/access/check/auth/evaluateAuthorization, not authentication
/overbooking-configs/{scopeKind}/{scopeId}/centers/{id}/overbooking-config, /professionals/{id}/overbooking-configPolymorphic form covers every scope in one route
/availability/{subjectType}/{subjectId}/professionals/{id}/availability, /centers/{id}/availabilityAvailability is a query across a subject, not a field on it
Polymorphic scoping (§09)

When one resource attaches to several parent types, use an explicit discriminator pair rather than forking the path: /overbooking-configs/{scopeKind}/{scopeId}, /subscribers/{subscriberType}/{subscriberId}/entitlements, /audit/entities/{entityType}/{entityId} — and, as of v4.2.0, /availability/{subjectType}/{subjectId}. Discriminator values are SCREAMING_SNAKE enums drawn from a single shared list: CENTER, SITE, PROFESSIONAL, ORGANIZATION.

The me alias

/patients/me/… resolves from the token, never accepts an id, and returns 403 not 404 when the caller isn't that kind of actor (a 404 would leak whether the record exists). Extended in v4.2.0 to /professionals/me, joining the existing /accounts/me and /patients/me.

10–13 · Payloads, time, collections, contracts
Field vocabulary
{ "id": "uuid", "publicId": "uuid", "countryId": "BF", …, "createdAt": "…Z", "updatedAt": "…Z", "version": 0 }
SuffixTypeExample
IdSingle UUID FKagendaId
IdsArray of UUIDssiteIds
AtISO 8601 instant, UTC, ZstartsAt
OnCalendar date, no timeexpiresOn
CountInteger tallymemberCount
CodeStable string identifiercountryCode
UrlAbsolute URLdownloadUrl
AmountMoney — always paired with currencytotalAmount

Kind vs. Type: Type = real business taxonomy (centerType, actType). Kind = internal plumbing discriminator (scopeKind, referentialKind). Booleans are never negated: ✓ isOverbooking, hasActiveSubscription — ✗ notPublished, disabled.

Time & collections

All instants ISO 8601 / UTC / Z. Where local wall-clock matters (it always does in booking), carry the zone as a sibling field — siteTimezone. Cursor pagination, not offset: { data: […], pagination: { cursor, nextCursor, pageSize, hasMore } }. Repeating a filter param means OR; different params mean AND.

Errors & idempotency
{ "error": { "code": "VERSION_MISMATCH", "message": "…", "details": […], "traceId": "uuid" } }

Codes are SCREAMING_SNAKE, stable forever, never reused with a new meaning. Every POST a client might retry accepts X-Idempotency-Key.

14–15 · Lifecycle, events, migration

Major version in the path, nothing else. Removing/renaming a field, narrowing a type, or a new required field needs v3; adding an optional field, a response field, an endpoint, or (documented-tolerant) an enum value ships into v2.

Events mirror their endpoint

{aggregate}.{pastTense}, lowercase, dot-separated, aggregate singular. If POST /appointments/{id}/cancel exists, the event is appointment.cancelled — never booking.cancelled, never appointment.cancel.

Fixed in v4.2.0 (register §05): entity.* (4 events, named no real aggregate) split per-aggregate into 12 events with corrected tense; clinical.correctedclinical_entry.corrected; appointment.no_showappointment.marked_no_show (was reading as a state, not an occurrence); completeness_rules.updatedcompleteness_rule.updated (plural aggregate).

Two generations, one winner (migration)

The catalogue historically held two parameter styles — {braces} (OpenAPI, the majority) and :colons (Express routing syntax leaking into specs). This catalog already uses {braces} exclusively.

Permission action vocabulary (register §04)

The catalogue uses a permission action vocabulary of RESOURCE+ACTION strings. Core tiers, always available:

TierActionsRule
CoreVIEW · CREATE · UPDATE · DELETE · MANAGEAlways available. MANAGE implies the other four.
ScopedVIEW_SELF · UPDATE_SELFSame action, narrowed to the caller's own record.
LifecycleACTIVATE · ARCHIVE · SUBMIT · PUBLISH · RETIREState transitions on a versioned or approvable entity.
DecisionDECIDE · RESPOND · RESOLVE · TRANSITIONThe four approval verbs above.
DelegationGRANT · REVOKE · SUSPENDConferring or withdrawing rights on someone else's behalf.
BulkIMPORT · EXPORT · MERGEOperations over many records at once.
BespokeSIGN · CERTIFY · CHECK_IN · …Allowed only where the action is legally/clinically distinct and a reviewer would genuinely grant it separately.

The test: a bespoke action earns its place if you can name a real role that gets it while being denied UPDATE on the same resource. PRESCRIPTION+SIGN passes (a secretary drafts, only a prescriber signs); MODULE+COMPOSE and ORGANIZATION+MANAGE_BRANCHES don't — they're MANAGE wearing a hat.

Applied in v4.2.0: 14 of the 73 original actions (52 of them used by exactly one resource) folded away as failing this test — see the Architecture v5 tab's Open Questions for the full fold list and the reasoning for what was deliberately not folded further (most singles passed the test on a real, spec-grounded actor distinction — collapsing them into MANAGE/UPDATE would trade away real, auditable granularity that a healthcare RBAC system needs).

🗂 Domain Naming Register v1.0

Who owns which resources, what their identifiers look like, and where two domains currently reach into the same root. One entry per domain, seven slots each, so any two are comparable at a glance.
Authorization scope ladder

Every endpoint declares exactly one. Scope is what separates SuperAdmin from Center from Site from Provider — not the URL, and not the role name.

No domain matches that filter.

🧮 Identifier Derivation

One root name generates five identifiers, mechanically. Pick the plural kebab-case root once; everything else below is derived, not invented.
Status: Waiting