Platform Glossary
Single source of truth for terminology. Every term used in code, docs, or UI lands here with a one-line definition and the architectural category it belongs to.
Authority. Where the codebase, comments, or other docs disagree with this glossary, the glossary wins and the divergence is a rename target — not a competing definition. The taxonomy here is what we are building toward; drift gets corrected, not accommodated.
Scope. Cross-cutting terms that span multiple features or layers. Domain-specific terms that only matter inside one feature live in that feature's own docs. The test for inclusion: would mis-defining this term cause confusion across two or more files?
How to read this doc
- Each term has a one-line definition followed by a pointer to the canonical deep doc.
- Bracketed tags
[arch],[domain],[compliance], etc., classify the term. - The Forbidden terms section at the end is the enforcement teeth — any usage of a forbidden term in new code or docs is a rename target.
Architectural building blocks
These define how every feature in the platform is structured.
Capability [arch] An internal Go interface that does one bounded thing, with a stable contract. The unit of architecture the platform is built from. Examples: email.Channel, video.Provider, pdf.Renderer, ai.LLM, webhook.Deliverer. Lives in internal/core/{name}/. Has a stable interface, a swappable implementation strategy, and standard hooks for audit, metering, permission gating, and error classification.
Feature [arch] User-facing functionality, composed of one or more capabilities. What the user sees, what the marketing page lists, what the changelog announces. Has UI surfaces in clinic, portal, or console. Built ON TOP of capabilities — never IS a capability. Examples: telerehab session, appointment booking, treatment plan editor, AI clinical drafting, webhook subscriptions marketplace.
Entitlement [arch] [billing] What the platform sells per plan/tier — gates access to features (boolean gates) and sets quotas (numeric limits). The umbrella covers two parallel families in the schema, both conceptually entitlements but kept distinct because their runtime semantics differ:
Boolean family ("is X enabled?") — renamed in 1C.9:
entitlements— catalog of boolean entitlements (e.g.,telerehab_enabled,ai_assistant,custom_domain).tier_entitlements— which plans grant which entitlements.organization_entitlements— per-org current state, including overrides above the plan's defaults.
The snapshot tables that ride alongside subscriptions follow the same naming: organization_subscription_entitlements, patient_tier_entitlements, patient_subscription_entitlements. The Go gates that read these tables are middleware.RequireTierEntitlement (plan-level) and middleware.RequireOrgEntitlement (regulated org-level projection); on principal.Subject, the corresponding methods are HasTierEntitlement and HasOrgEntitlement.
The content.* namespace (content.prescription_play, content.premium) is the patient-facing content-access family: non-regulated boolean entitlements assigned to patient_tiers and resolved at play/browse by the catalog access gate (internal/core/access + the patientsubscriptions resolver), not by the RequireTierEntitlement middleware. content.prescription_play (+ an active subscription) gates clinician prescriptions; a catalog entry's required_entitlement (e.g. content.premium, NULL = free) gates self-enroll/standalone content. A patient_content_grants row is the per-patient override on the catalog branch (free OR grant OR tierHas(code)) — a comp/promo/legacy-ownership grant unlocks specific content independent of tier; it never applies to prescriptions. content.grant is the staff permission (not an entitlement) gating that grant/revoke. See patient-catalog-and-access.md.
Quota family ("how many X allowed?") — unchanged in 1C.9, no rename:
limit_definitions— catalog of quota entitlements (e.g.,max_emails_per_month,max_locations,max_webhook_subscriptions).tier_limits— which plans grant which quotas with what value.organization_subscription_limits— per-org snapshot of quota grants.organization_subscription_overrides— per-org per-quota adjustments above plan defaults.
The Go gate is middleware.EnforceLimit(code) which reads from organization_subscription_limits (or its overrides). At runtime, the metering layer (1C.7) reads the same source to populate usage_quotas.limit_units for live counter checks.
Why two families instead of one merged table: boolean entitlements gate via binary check (return 402 if disabled); quotas gate via counter check (return 402 if exceeded). Different code paths; clean schema split mirrors that. The boolean half got renamed because features/capabilities collided with architectural words ("Feature" = user-facing functionality, "Capability" = internal Go interface). The quota half uses limits, which doesn't collide with anything architectural — no rename needed.
Implementation strategy [arch] How a capability is implemented. Not a thing on its own — a description of where the work happens. Five flavors:
- Internal Library — pure Go code in our process, no external calls. Examples: PDF rendering, HMAC signing, encryption helpers, JSON schema validation.
- Curated Provider (Cat A) — external API call with platform-owned credentials. Examples: SES for email, Daily.co for video, Anthropic for LLM. Clinic never sees the provider name; switchable behind the capability interface.
- Connected Account (Cat B) — external API call with clinic-owned credentials, configured by clinic admin via OAuth or API key. Examples: Google Calendar, Slack, HubSpot, Salesforce.
- Outbound Webhook (Cat C) — POST to a clinic-configured URL with a signed payload. Examples: Make.com scenario URL, Zapier webhook, custom clinic backend.
- In-browser code — runs on the patient's device, returns a value to our API like any other input. Examples: TensorFlow.js posture analysis, MediaPipe goniometer.
A capability picks one strategy or composes several. The same capability may have multiple Curated Provider impls (e.g., ai.LLM could route to Anthropic or OpenAI), selected at runtime by config.
Implementation-strategy code identifiers (the internal/core/capabilities wrap helpers) follow the same axis: Curated Provider impl strategy is wrapped by WrapProvider (unmetered) or WrapMeteredProvider (metered); Outbound Webhook by WrapOutbound; Internal Library by WrapInternal. Connected Account and In-browser code don't have wrap helpers — Cat B credentials are platform-resolved on a separate per-org code path, and In-browser code runs on the client and returns a value through the regular API surface.
Integration [arch] External system touchpoint. An umbrella term covering the five external categories: Curated Provider (Cat A), Connected Account (Cat B), Outbound Webhook (Cat C), Inbound Webhook (Cat D), External API Access (Cat F). Internal Events (Cat E) are NOT integrations — they're internal.
Internal Library [arch] A Go package that implements a capability with no external API calls. Pure code in our process.
Principal-type-agnostic primitive [arch] Cross-cutting design property that every foundation primitive (capability framework, metering, audit, quotas, RLS, permissions, events) treats all principal types the same — humans, agents, service_accounts, and the system principal flow through identical code paths. Audit attribution carries actor_id + actor_type for observability; the primitive's behavior does NOT branch on actor type.
Rationale: the platform supports four principal types (1B.1) and any of them may trigger any operation in principle (a clinic admin sends an invitation; an AI agent generates a treatment plan draft; a service_account syncs patients via Cat F API access; the system principal runs a scheduled cron). If primitives accidentally hardcoded "human" assumptions, agent and service_account flows would silently fail to audit / meter / authorize correctly when they light up.
How to apply: when designing or reviewing a foundation primitive, ask "would this work the same way if the actor were an agent or service_account?" If not, the primitive has a hidden human assumption that needs surfacing. This property is enforceable via integration tests that run the same operation as different principal types and assert identical observable behavior.
Exceptions are explicit and documented (e.g., OAuth flows in 1C.5 require human consent because the dance involves a browser redirect — call this out where it occurs). Default state is principal-type-agnostic.
Integration categories
The six distinct categories of touchpoints (five external + one internal). Confusing them is a common source of design drift.
Cat A — Curated Provider [arch] [integration] External service we depend on, with platform-owned credentials. Clinic never sees the provider name. Switchable behind a capability interface (s.email.Send(...) not s.ses.Send(...)). Credentials in env / Secrets Manager + the foundation provider-resolution table (platform_service_providers) for per-org overrides (e.g., SES sender identity, Twilio sender ID, Daily.co subdomain — available on either tenancy mode as a paid customisation). Examples: SES, Daily.co, Twilio, Anthropic, Stripe, AWS S3, Clerk, KMS.
Cat B — Connected Account [arch] [integration] External service the clinic admin connects via OAuth or API key. Clinic owns the credentials; we call the third party's API on the clinic's behalf. Lives in organization_integrations table (per-org instance rows referencing the platform-defined integration_services catalog). Examples: Google Calendar, Microsoft 365, Slack, HubSpot, Salesforce, future EHRs.
Cat C — Outbound Webhook Subscription [arch] [integration] Clinic configures a URL + signing secret + event-type filter. We POST signed payloads to that URL when matching events fire on events.Bus. The same row type whether the URL points at Make.com, Zapier, n8n, a custom clinic backend, or a Slack incoming webhook — they're all just URLs to us.
Cat D — Inbound Webhook [arch] [integration] Third party POSTs to one of our /webhooks/{provider} endpoints. We verify signature per provider's scheme, update internal state, and emit Internal Events. Examples: Stripe payment_intent.succeeded, SES bounce/complaint notifications, Daily.co recording.ready, Clerk user lifecycle, Google Calendar push notifications.
Cat E — Internal Event [arch] Domain event fired on events.Bus (1A.9) when state changes. Internal-only — no network call. Subscribers include the audit log, the notification dispatcher, the outbound webhook dispatcher, the automations engine, and future AI agent action logs. Single canonical event registry; webhook docs and automation triggers reference the same registry.
Cat F — External API Access [arch] [integration] External systems calling our REST API as authenticated principals via service-account API keys. Direction is them → us, but distinct from Cat D (which is webhook-shaped notifications). Cat F is full CRUD: an external system (clinic's custom backend, Zapier with action steps that fetch data, EHR sync, partner integration) authenticates with Authorization: Bearer sa_live_..., our auth middleware resolves the key to a principals.id (subtype service_account), and the request flows through normal RBAC + RLS like any other authenticated call. The service_accounts table (1B.1, schema shipped) holds these principal records. Operational flow (key creation, revoke, rotation, admin UI, per-key scoping) is documented in foundation.md → Deferred Foundation Extensions; ships when first concrete external system needs it. Don't confuse with integration_services (Cat B catalog) — that's the menu of "services we connect TO"; Cat F is "actors who call IN to us."
Identity & tenancy
The actor and tenancy model. See decisions.md → Why principals as the root identity for full rationale.
Principal [identity] The root identity in the system. Every actor — human, AI agent, integration service account, scheduled system job — is a row in principals. There is no users table.
Human [identity] A principal subtype representing a person. Profile data (name, email, timezone) lives in humans (PK = principal_id). Patient identity, caregiver, specialist account — all live here.
Agent [identity] A principal subtype representing an AI agent. First-class actor with its own audit trail, permissions, and RLS scope. See ai-agents-runtime.md. Future feature surface; primitive shipped at 1B.6.
Service Account [identity] A principal subtype representing a non-human integration actor (system jobs, API integrations).
Organization (Org / Clinic / Tenant) [identity] [tenancy] The multi-tenant boundary. Every clinic-owned table has organization_id. Patients can hold memberships in multiple orgs. The clinic is the GDPR data controller; the platform is the processor.
Location [identity] [tenancy] Physical/logical site within an organization. One org has one or more locations. Used by scheduling and operational segregation. See 1B.14.
Membership [identity] A principal's relationship with an organization, captured in organization_memberships. Carries a role assignment.
Permission [identity] [security] A per-organization gate code (patients.read, appointments.write, etc.). All authorization decisions go through permission codes — never role-string compares. See reference/rbac-permissions.md.
Role [identity] [security] A bundle of permissions. Roles are per-org; system role templates seed new orgs.
Superadmin [identity] [security] Platform-level role granted via platform_memberships. Human-only by CHECK constraint. Bypasses RLS. Reserved for the platform team. Not a tenant role.
Break-glass [identity] [security] [compliance] Time-bound elevated cross-org access for support, time-limited, justified, audited, with always-on clinic notification. The pattern that lets identifiable cross-tenant access happen without joint-controllership risk. See 1B.11.
Clinical domain
Terms that describe what the platform does for clinics and patients.
Offering [domain] A clinical service the clinic offers patients (e.g., "Initial Assessment," "Group Class," "Follow-up Consultation"). leo calls it serviciu / appointment_template. Replaces the previous "service" naming, which collided with the architectural sense of the word.
Shipped 000041 (2026-08-03) — catalog identity only. The F2.1 stand-in is the offerings row (org-scoped title / slug / description / specialty / default duration / publication state) plus offering_specialists (the roster the assignment engine walks, carrying priority). offering_forms (which form templates attach at which Form slot) is specified but not yet built — it needs form_templates as an FK target and lands with F3.4. No pricing, no plans, no products, no purchase path — F2.2 (packages — reserved name Offering package, see the Clinic operations section) and F2.3 (products) stay deferred. An offerings row is not something a patient buys; it is something a patient gets booked into.
Two orthogonal publication flags, deliberately not one enum: published means configured and usable at all, is_public means advertised on the self-service booking page. Published-but-not-public is a real state — the clinic books the service on the patient's behalf without advertising it. Effective patient visibility is published AND is_public, enforced in the RLS SELECT policy rather than left to a query.
Delivery mode is not a property of the Offering. Whether a service happens in a clinic room or over video is decided one layer down — calendars.location_id (F4, NULL = remote/telerehab) — and recorded on the booking (appointments.location_id + channel). One Offering can be delivered at two sites and online through three calendars. Locations label availability; they never partition the catalog.
The Offering is the configuration spine of the clinic-operations stack. calendars.offering_id (F4), appointments.offering_id (F5) and the form-generation mechanism (F3) all resolve through it — three in-scope features hold NOT NULL FKs into it, which is why the catalog identity shipped even though the commerce half of F2 did not. See platform-completion.md and leo-port-map.md §2.
An Offering is NOT an access-offer. access_offers (F14, shipped 000035–000038) is a merchandised bundle of content grants + tier subscription, minted by a campaign claim or a shop order, resolved by the play/browse gate. An Offering is a clinical service that gets scheduled. Two concepts, one word; conflating them already cost one wrong scope decision.
Protocol [domain] One patient's enrolment in one programme — the row that says a person is currently doing something. Shipped as the protocols table (migrations/core/000023_sessions.up.sql), renamed from assignments on 2026-05-23 when three-tier copy-on-derive retired the "assigned to a shared program" framing.
It is the union of both kinds, and that is the point of the word. protocols.kind is (prescription | enrollment) — a specialist prescribing, or a patient enrolling themselves. Any surface that asks "what is this clinic currently running" wants both, and asks for protocols. See Prescription and Enrollment for the individual senses.
A protocol wraps the workflow around a patient-instance programme: cadence, lifecycle (active | paused | completed | ended), approval, and source-tracking. program_id points at the patient's OWN deep copy (1:1 with this row), never the library template; source_program_id records which template it came from and is nullable, because that template can later be deleted.
Do not call this an "assignment" in user-facing text. Two reasons, both live: the word does not cover self-enrolment, and in a staff UI it reads as a to-do assigned to the logged-in user. The domain, the routes (/v1/protocols, /v1/patients/{patientId}/protocols) and the Clinic nav all say protocol; a heading that says "Assignments" is a leftover from before the rename, not an alternative.
Enrollment [domain] A patient's self-initiated start of a guided program. Shipped and live in production as protocols.kind = 'enrollment' (migrations/core/000023_sessions.up.sql), the sibling of kind = 'prescription' (specialist-assigned). An enrollment forbids cadence_kind — it tracks course progress and carries no adherence denominator. Patient path is POST /v1/me/protocols, gated by the protocols.enroll permission code.
The word is taken. enrollments previously stood in the Forbidden-terms table as the reserved rename target for the deferred service_plans concept. That reservation is retired as of 2026-08-02 — a second, unrelated meaning cannot be layered onto a term with production tenure in a DB CHECK constraint, a permission code, and a patient-facing route. The deferred concept's reserved name is now offering_packages. No enrollments table will be built.
Publish update [gesture] The explicit version stamp of the template-update pull model (2026-08-23): converts a template's accumulated edits — dirty markers on its sessions' content and on the program's structure — into version bumps (sessions.content_version, programs.structure_version), which is what makes derived patient copies read as outdated and show their update affordances. POST /v1/programs/{id}/publish-update (whole subtree) and POST /v1/sessions/{id}/publish-update (standalone library session); audited under its own PUBLISH_UPDATE verb. Distinct from publish (the draft→published status flip): templates are editable in place at any status, and the stamp is the moment staff say "this is now the version patients should have" — a 5-step editing session produces one offer, not five. Copies pull via the apply endpoints (APPLY_UPDATE); nothing propagates silently. Spec: template-updates-and-builder.md.
Severed [state] A patient-copy session whose content was customized for that patient — sessions.content_severed_at set (once) by the first dose/content edit on a patient_specific row. A severed session never shows a content-update offer again and the apply endpoint refuses it: customized-for-this-patient means permanently theirs. The sever is a stamp, not an erased pointer — source_session_id survives, because it is also the matching identity the program-structure sync places rows by (erasing it would re-copy the session's template twin in beside the customized one). The program layer severs separately: any structure edit on an instance NULLs programs.source_structure_version, closing the structure-update door while individual sessions keep their own content lineage.
Rename status — services → offerings is now in flight (deferral condition met 2026-08-02)
The deferral condition was "until that area is built." It is met: the F2.1 offerings stand-in is in scope in platform-completion.md. The table is created as offerings from day one — there is no interim services table and therefore no later rename of five tables and every FK under the forward-only freeze.
What remains is a doc sweep of the pre-existing services naming, done with the F2.1 build: the F2 feature spec still filed under features/services/, data-model.md Area 3 ("Service Catalog") and its service_specialists / service_plans entries, and features.md's service_forms junction — all superseded by offerings / offering_specialists / offering_forms. Architectural code added in the meantime never uses the old terms.
Appointment [domain] A scheduled session between a patient and a specialist (or a group session). Has a start/end time, a location or video room (location_id NULL = remote/telerehab, P40), an Offering it was booked from, and an explicit status machine (P33). Spelling is cancelled, and cancellation is attributed rather than flat — the adherence denominator must be able to exclude clinic-attributable cancellations. The authoritative enum, transition graph and pair CHECKs live in appointments-substrate.md; do not restate the value list here, it drifts. Patient identity on the row follows Two-phase booking identity (Clinic operations section). Not built yet — no appointments table exists today.
Online consultation [domain] An appointment delivered over video — appointments.channel = 'online_live', and calendars.location_id IS NULL. Say "Online" in every user-facing surface, which is what the delivery label already said in both apps and both languages; the consent that gates it is the Online consultation consent and the ledger purpose is displayed as Online Consultation.
The code stays telemedicine — it is a consent_purposes.code with live consents rows against it, so renaming it is a migration that buys nothing. Only the display names were aligned (2026-08-20).
Deliberately NOT "video consultation". video_recording is a separate consent gating a separate thing, and "video consultation consent" beside "video recording consent" is a worse collision than the one being fixed.
Not telerehab, which is the one it gets confused with. Online consultation is a specialist seeing you over video; telerehab is exercising alone at home with nobody watching. Two different consents gating two different features. The seeded catalog description for telemedicine said "telerehabilitation session" until 2026-08-20, which is how a clinic testing the gate published the wrong agreement and saw nothing happen.
Treatment Plan [domain] A prescriptive document a specialist creates for a patient — exercises, instructions, schedule. Distinct from an Offering (which is the catalog item) and from an Appointment (which is the scheduled session). Treatment plans require a specialist signature and are immutable once signed.
Form [domain] A questionnaire — intake, clinical, consent, custom — completed by a patient or specialist. Signed forms are immutable.
Patient Profile [domain] Patient's portable, patient-owned identity (patient_profiles — no organization_id). Same row visible at every clinic the patient is enrolled at, propagating updates without per-org duplication. See P6.
Patient (per-clinic record) [domain] A patient's per-clinic clinical identity (patients). Holds clinical state scoped to one clinic — distinct from the portable Patient Profile.
Consent [domain] [compliance] A per-patient, per-org, per-purpose grant or withdrawal recorded in consents. Consent at Clinic A does not extend to Clinic B. Self-withdrawable purposes (marketing, analytics) toggle per-row.
Profile sharing [retired] A forbidden term as of 2026-08-20. There is no per-field disclosure gate on the portable Patient Profile: a clinic reads it because the patient is registered there, and registering IS the disclosure. Say "the patient is registered at this clinic", never "the patient shared their profile". See P8 — retired.
Caregiver [domain] A human who manages a patient's account (parent for a minor, family member for an elderly patient). The patient may not have their own account.
Reported pain location [domain] The optional body location a patient attaches to an in-exercise pain event (session_pain_events), captured in the Portal PainSheet. Two mutually-exclusive forms: reported_region_id — a pick from the platform exercise_body_regions catalog, shown as chips when the exercise targets 2+ regions (a single-region exercise asks nothing); and reported_region_note — a free-text "altă parte" escape for off-region / referred / compensation pain the body-region vocabulary can't place. Both NULL → the clinic pain map derives the region(s) from the exercise's body_region tags (the pre-feature behaviour). A region pick is the more specific signal: it wins over derivation and lands on the body-map silhouette; a free-text note is listed separately under the silhouette, never plotted. Patient-reported informational data — Class I MDR posture (the specialist interprets it; nothing algorithmic consumes it). Distinguished from session_pain_events.side, which is the anatomical side the exercise dose was working, not a patient-drawn location.
Clinic operations (F1–F6)
Vocabulary that arrives with the clinic-operations stack — Specialists, Offerings, Forms, Scheduling, Appointments, Documents. Specialists / Specialties (000040), Offerings (000041), Forms + Document categories (000042–000044), Calendars (000045), Appointments (000046) and Documents (000047–000048) are all BUILT in the repo — and none of them is on staging or production, which run 000038 and 000039. So these entries describe shipped code against an unshipped schema; treat "does the table exist" as a question about the environment, not about the name. Where a design question is genuinely open it is marked Open rather than guessed.
These features are ports of a live system (restartix-leo-* + restartix-intakes), so several entries carry an operating rule extracted from running code that appears in no spec on either side. Build plan: platform-completion.md; evidence and per-feature port plans: leo-port-map.md. Offering, Appointment, Form and Treatment Plan are defined above under Clinical domain.
Booking client ID [domain] [security] The identifier for a booking browser session: a server-signed HttpOnly cookie, minted by the API and persisted on appointments.booking_client_id. It keys the public-booking cooldown and hold ownership. Never caller-supplied — leo's clientId is read from body > query > cookie > generated, is never validated and never persisted, and per the port map the dashboard regenerates it after a successful booking, which defeats the 24h cooldown it exists to enforce. A client that can choose or rotate its own identifier is not a rate-limit subject. See port map C13.
Calendar [domain] The booking configuration of an Offering (F4): slot duration, slot gap, cooldown, minimum lead time, booking window (explicit open/close XOR rolling horizon days, enforced by a DB CHECK — leo enforces it only in a client-side save handler), assignment strategy, publication + slug, plus its own roster (calendar_specialists, carrying priority) and attached forms (calendar_forms). Carries offering_id NOT NULL and location_id NULL (NULL = remote/telerehab, P40). Ported from restartix-intakes' schedules table, where the live booking configuration runs today.
Three distinctions: not the calendar component in packages/ui; not the staff month/week grid (a UI surface over appointments); and not availability — availability belongs to the specialist (Weekly hours + Schedule override), while the Calendar decides how that availability is sliced into bookable slots for one Offering. appointment_types is the losing name for this entity and still appears in older scheduling / appointments / integrations feature docs; calendars wins.
Document category [domain] [config] One kind of paperwork, as a clinic defines it — a row in document_categories, per-org, soft-deleted. It is what form_templates.category_id and pdf_templates.category_id both name, and its key is denormalised onto forms, offering_forms, calendar_forms and appointment_documents as category_key.
It replaced a seven-value CHECK on form_templates.type, ported from leo's FORM_TYPE_TO_SLOT map. The whole semantic content of that enum was three properties, and all three are now columns: sort_order (the render and attach order — formerly a slotOrder array in Go), cardinality (one | many per offering — formerly singleCardinalitySlots), and filled_by (patient | staff). A fourth, generatable_on_appointment, replaced a hardcoded {report, medical_prescription} pair in the documents service. The word "survey" never carried any of them.
filled_by is the axis the enum could not express, and it is why the change was made. The old model listed five attachable slots and excluded report and medical_prescription on the grounds that neither is handed to a patient at booking — true about when a form is created, wrong about whether it may be configured. It left the one document a specialist writes during a consultation with no way to be assigned to an Offering at all. patient categories materialise at booking and appear on the patient's wall; staff categories materialise when the appointment enters inprogress and never reach the portal.
Behaviour is never keyed off a category NAME. The two rules that used to read type == "medical_prescription" now read pdf_templates.requires_signature, which is DERIVED from whether the layout draws a signature block — the same contract requires_national_id already had. A rule attached to a name holds only for documents somebody remembered to name that way: a clinic's signed medical certificate got no guarantee, and a signed report was impossible.
system_key is set on the seven rows seeded into every new org (disclaimer, survey, parameters, analysis, advice, report, medical_prescription) and exists for exactly two jobs, both about resolving identity across a rename: the legacy import, and cross-org template copy. It is not an enum in disguise — nothing in the request path may branch on it. The seeded rows are ordinary org-owned rows a clinic may rename, reorder, retype or delete, exactly as custom_fields' seeded library is; deletion is bounded by the RESTRICT FK from both template tables rather than by a protected tier.
Written behind document_categories.manage (admin only) — its own code rather than form_templates.manage, because the taxonomy governs both form templates and PDF templates and borrowing either domain's permission would grant authority over the other sideways. Reads are ungated beyond org scope: a category's title is the heading above a form, so every renderer including the patient's must resolve it.
Specialist title [domain] [config] One PROFESSION a clinic employs — a row in specialist_titles, per-org, soft-deleted. "Medic", "Kinetoterapeut", "Psiholog". Added 2026-08-10.
It decides which of an Offering's attached forms and document layouts a given clinician receives, via a nullable specialist_title_id on offering_forms and offering_documents. NULL means every specialist, which is what every attachment carries until a clinic creates its first title — so the concept is invisible until it is wanted.
Why the rule sits on the ATTACHMENT and not on the category. A doctor may issue a medical report and a prescription; a therapist may not. Both write a report — but not the same report: Raport medical and Raport de kinetoterapie are two layouts in one Document category. An earlier draft had a title declare which categories it could issue, and it fails on that first real example, because both professions issue report and a category gate lets each reach the other's layout. The discriminator is the template.
Three things it is not. Not authorization — specialists.human_id is nullable, so a calendar-only specialist has no account and no permissions, and the rule could not live in RBAC; permissions gate who may press generate, the title gates what may be issued in a given specialist's name. Not a Specialty — that is many-to-many, so "which of mine decides what I may sign" has no answer, whereas a title is single-valued, which is the only reason it can carry this. Not specialists.title, which is a free-text display honorific ("Dr.") rendered under a name on the public roster; the two look alike and are not — "Dr." is what a patient reads, "Medic" is what the system routes on.
No flags on the row, deliberately. A boolean like can_issue_medical_documents is the enum document_categories deleted wearing a different hat: it cannot express "may issue a work-leave certificate but not a prescription" without a second column. A title says only what it is called; what it may produce is said by the attachments.
Written behind specialists.manage. Creating one grants no paperwork authority by itself — which layouts a title receives is decided by attaching them to an Offering under offerings.manage — so widening what a profession may issue takes two grants. Read by any principal of the org and by its patients, because a title is a word rendered beside a clinician's name on a booking page.
Custom field [domain] A per-org, versioned field definition used by form templates and entity metadata (F3: custom_fields / custom_field_versions / custom_field_values; P19 + P24). Scoping is UNIQUE (organization_id, entity_type, key) — never global. leo's meta_field.key is globally unique, which is exactly why its cross-franchise template copy leaves templates pointing at another tenant's field definitions. Field keys are immutable once assigned, because PDFs and exports reference them.
A custom field of type national_id never reaches the generic value store. CNP is pii_regulated → encrypted BYTEA via internal/core/crypto, stored once on the patient-owned patient_profiles, egressed only through an explicit data-classification.md target for the PDF renderer, and revealed only through the permissioned, audited /national-id endpoint. A custom_field_values.value TEXT column can never legally hold one: the type either routes to the dedicated encrypted column or is rejected outright. Settled 2026-08-02; leo stores CNP as plaintext meta_value.value and prints it on every report (port map G22).
Form instance [domain] One patient's filled-in copy of a Form template — a forms row (F3). Carries a fields JSONB snapshot of the template version taken at creation plus template_version, the answer values, a status of pending | in_progress | completed | signed, and the signing columns. The snapshot is the single most important structural difference from leo, which has none — there, editing a template retroactively rewrites how every historical form renders. Signed instances are immutable: any mutation returns 409 Conflict, enforced at handler and service layers (P14b).
Form slot [retired term]Retired 2026-08-10 — use Document category. It named the attachment point binding a Form template to an Offering or Calendar, with a five-value vocabulary and cardinality ported verbatim from leo's FORM_TYPE_TO_SLOT.
The derivation rule it described SURVIVES and is unchanged: the category is read from the template at attach time, never chosen — the attach endpoint takes no category, and the primary key is (offering_id, form_template_id) because a template has exactly one. An early revision of F3.4 accepted it from the request; nothing downstream read it, so the only reachable effect of a mismatch was a wrong queue order and a single-cardinality slot consumed by the wrong form.
What did not survive is the closed vocabulary, and the reason is worth keeping. The chk_offering_forms_slot CHECK admitted five values and refused report / medical_prescription with 409 form_type_not_attachable, on the argument that neither is handed to a patient at booking. That argument was right about when a form is created and wrong about whether it may be configured: it left the one document a specialist writes during a consultation unassignable, so a clinic could generate a PDF from a form nothing had created. leo assigns both on its appointment-template exactly like the others.
filled_by is what the CHECK was really expressing, and it is now a column a clinic sets. analysis carrying createValues: false in leo — generate the form but do not open it for answering — is why that category seeds as staff rather than patient.
Form template [domain] The versioned, publishable definition of a form (F3: form_templates + form_template_versions; P18). Publishing mints a version; instances snapshot the version they were created from. Distinct from a Form instance (one patient's filled-in copy) and from the shipped legal_document_templates (1B.10 platform legal text — different lifecycle, different owner). Cross-org copy remaps fields by System field key within the target org; a dangling cross-org reference fails the copy rather than silently binding to another tenant's definitions (port map C7).
Hold [domain] [arch] A short-lived reservation of a bookable slot, taken while a patient or staff member completes a booking. Redis state, not a table — P44 forbids session-mode Postgres features, so the hold store is Redis with a Lua-atomic heartbeat on internal/core/locks, fanned out to clients over internal/core/sse. Keys are namespaced with cache.OrgResource(orgID, ...): leo's hold:{scheduleId}:{slot}:{openingId} and client:{clientId}:holds carry no org dimension, which is the P42/P45 scope-must-match-visibility failure (port map C13).
Heartbeat budgets differ by actor on purpose — staff ~16 minutes (they may need to find or create a patient mid-booking), patients ~100 seconds. A hold is not a booking: the durable record is the appointments row plus the DB-level double-booking exclusion constraint, never the hold.
Medical prescription (document) [domain] The rețetă medicală — a per-appointment PDF generated from a signed Form instance against a frozen pdf_templates version (F6). Named medical_prescription wherever it is an identifier (the seeded document_categories key, carried on appointment_documents.category_key and offering_forms.category_key); bare prescription is reserved for the shipped exercise-program sense — see Two senses of prescription. Generation refuses without a specialist signature (typed 422), and the signature is embedded as base64 because PDFs are self-contained with no external URLs. Audience does NOT differ by document type: leo shows all fields to the patient on a prescription while reports prune is_private, and that rule (leo D3) was retired 2026-08-07. One rule holds everywhere — private is private — because a promise of "staff-only" that one document type breaks is not a weaker promise, it is no promise.
Offering package [domain] [billing]Reserved name — nothing built, and shipping is not committed. The deferred F2.2 concept: a bounded, purchasable bundle of N appointments of one Offering with a validity window, decremented as appointments are consumed ("6 physio sessions, valid 3 months"). This is the rename target for the legacy service_plans / patient_service_plans naming, replacing the retired enrollments reservation (that word is taken by the shipped protocols.kind = 'enrollment').
Why this name: it is domain-prefixed to the entity it belongs to, matching offering_specialists / offering_forms; and it collides with nothing — not enrollment (shipped protocol kind and the protocols.enroll permission code), not access_offers (F14 content-grant bundle), not the recurring-access family (patient_tiers / patient_subscriptions), not bare "plan" (forbidden — always qualify). "Package" alone is an overloaded engineering word; prefixed with offering_ it reads unambiguously in a DB/domain context, and neither string appears anywhere in the schema today.
Open: whether the concept ships at all, and whether "N sessions of Offering X remaining" needs its own decrementing balance or falls out of the shipped patient_subscriptions / access_offers chain (platform-completion reconciliation #7; port map §8). The reservation exists so nobody re-uses a taken word — it is not a commitment to build.
Schedule override [domain] A dated exception to a specialist's Weekly hours (specialist_schedule_overrides, F4): absolute TIMESTAMPTZ range + availability BOOLEAN + location_id NULL. Two rules ported from the live engine, absent from every spec on both sides:
- An override REPLACES that local day's weekly hours — it never merges. If any override exists for a date, that date's weekly rules are skipped entirely and only the override's
availability = trueintervals apply. - An override with zero intervals blocks the whole day, encoded server-side as a single
00:00–23:59availability = falserow. That is how "block Tuesday" is expressed.
Open — the scope column. The live Intakes system scopes overrides per schedule (scheduleOpeningOverrides.scheduleId NOT NULL, with production rows, having explicitly replaced an unscoped predecessor); the port map recommends calendar_id UUID NULL (NULL = all calendars); data-model.md Area 4 and the specialists feature spec disagree with both and with each other. Blocks the availability migration — port map §8.2.
Specialist [domain] A healthcare provider within one organization (specialists, F1) — the bookable clinical actor. Org-scoped (organization_id NOT NULL) with human_id UUID NULL UNIQUE → humans(principal_id), where NULL is an explicitly designed state: a "calendar-only" specialist is bookable without a login. That is why appointments.specialist_id FKs specialists and not principals (P9) — a principal FK would make every calendar-only specialist unbookable. One person working at two clinics has two independent Specialist rows.
Distinct from a Membership (a principal's role in an org — an admin is a member, not a Specialist) and from a Human (the person). Bookability is derived, never a flag: scheduling_timezone IS NOT NULL (P23) AND scheduling_active AND weekly hours exist, exposed with a machine-readable reason. scheduling_active = false removes the specialist from availability computation by construction and is kept strictly separate from humans.blocked — deactivating at Clinic A must not lock the person out of Clinic B. Shape per data-model.md Area 2.
Specialty [domain] A clinical specialty category (specialties, F1), M:M to Specialist through specialist_specialties. Per-org — settled 2026-08-02: specialties.organization_id NOT NULL, UNIQUE (slug, organization_id). Configuration data rather than clinical record, so hard delete is permitted, but pre-checked (409 with in-use counts) and audited.
Spelled specialty; leo's speciality is a misspelling that must not be carried across. Open: whether holding a Specialty gates roster assignment (422 when assigning a specialist to an Offering outside their specialties) or is taxonomic only — leo built the enforcing component and left it unimported (port map §8.13).
System field [domain] A platform-defined field identified by a stable system_key, as opposed to an org-authored Custom field keyed by key. It does two structural jobs: it is the join key that makes cross-org template copy safe (copy remaps system_key → the target org's key), and it is what binds a form field to a native patient_profiles column for auto-fill instead of to the EAV value store. Scoped UNIQUE (organization_id, system_key) — never global.
leo's patient-identity meta bindings (patient_meta_birthdate / _residence / _occupation / _sex) are dropped, not ported: patient_profiles already carries date_of_birth, sex, occupation, residence and phone as native columns (000006_patient_identity.up.sql). The exact column shape is an F3 build decision; the load-bearing property named here is the stable, org-scoped key.
Two-phase booking identity [domain] How an appointment references the patient across the public-booking boundary (F5). Phase 1 (booked): appointments.patient_profile_id is set — the portable, patient-owned identity — and patient_id is NULL, because someone booking from a public page has no per-clinic patients row yet. Phase 2 (onboarded): the per-clinic patients row is created and patient_id links. Both phases are pinned by CHECK constraints, and pre-onboarding contact data (contact_email, Booking client ID) lives on the appointment until the link happens. See appointments-substrate.md.
The pattern exists because portable patient identity and per-clinic clinical identity are deliberately separate tables (P6): booking cannot wait for onboarding, and onboarding must not fork a second identity.
Clinical capture [domain] An act by a clinician during a consultation that produces a clinical artefact from the patient's video — today a posture-grid photograph, later a computer-vision measurement (F5.5.8, documented not built). Tool-agnostic by construction: one clinical_captures shape with a tool discriminator and a version, because a measurement is only interpretable against the thing that produced it.
A capture's numeric output is an F16 measure point, which makes capture F16's fourth source alongside session_pain_events, session_runs and forms.values — so measure_key shares the custom_fields.key space, or a measured knee flexion and a form-asked one become two series that never meet. Side is a column, never a key suffix, and unit is required (degrees are a unit; custom_fields has none — this is where F16's scale-mixing trap becomes real).
Contrast leo, which is the rejected shape: measurements as {key}_left / {key}_right text in an answer store, images attached to whichever row was handy, and a filename as the only thing marking a capture as clinical.
Video room [domain] The provider-side meeting space backing one remote appointment (video_rooms, F5.5). State, flat, never partitioned (P41). Identified by an opaque room_ref derived from a server-side secret — never the appointment id, never URL-visible, never emailed; the provider's join URL is derived at token-mint time and not stored, because a stored URL is a capability at rest. Carries provider + provider_room_id and no vendor-named column: the Cat A adapter is the translation boundary, so a provider swap stays a factory change instead of a data migration.
Distinct from the Video session (what happened inside it). One room may host zero sessions — a room created for an appointment nobody joined is the normal shape of a no-show, and telling those apart is the point of keeping the two concepts separate.
Video session [domain] What actually occurred inside a Video room, reconstructed from video_session_events (F5.5) — append-only, range-partitioned monthly (P41). event_type is platform vocabulary (session_started, participant_joined, participant_left, session_ended) normalised at the adapter; provider event names are never stored raw.
A session is derived, never a stored row with a duration column. "Live now" is a query (started, no ended), and minutes come from join/leave pairs rather than session boundaries — because providers disagree on what "ended" means (Daily: last participant leaves, ≈20s grace; Whereby: fewer than two people for a minute), so a boundary-derived duration is not comparable across them.
Weekly hours [domain] A specialist's recurring availability (specialist_weekly_hours, F4): day_of_week + start_time / end_time as TIME in local wall-clock, plus location_id NULL (NULL = remote/telerehab, P40) and organization_id NOT NULL like every tenant-scoped table. State, not an event — flat, never partitioned (P41). Three rules ported from the live engine:
- Weekly hours are per-specialist and global; the dated exception is scoped. A specialist works 9–5 as a person but may block Tuesday afternoons for one Offering only — which is why the scoping column belongs on Schedule override, not here.
- Overnight rules split at local midnight — Fri 20:00 → Sat 02:00 becomes two windows. Without the split, both DST handling and day-grouping break.
- Spring-forward gaps are probed forward, not errored. A 02:30 slot on a day where 02:00–03:00 does not exist resolves to the next real instant.
The single-true-availability invariant — a specialist cannot be in two places at once — is enforced at the DB layer with EXCLUDE USING gist regardless of location_id: locations label availability, they never partition it.
Exercise taxonomy & pose tracking
Vocabulary that ships with F9.1 Phase 2 — the taxonomy axes layered onto the locked exercise-library design, and the per-exercise pose-tracking config that drives the AI pipeline. All decisions referenced below are locked in exercise-taxonomy-pose-tracking.md.
Clinical basis [domain] [compliance] Documented clinical rationale for why a tag association exists — the one-sentence justification (e.g., "lumbar tagged on this exercise because the primary movement is hip hinge with spinal loading"). Lives as clinical_basis TEXT on every tag-association row (exercise_tags, exercise_contraindications, exercise_instructions) and on every pose-config detail row (exercise_pose_landmarks, exercise_pose_metrics, exercise_pose_feedback_rules). Nullable at Class I MDR (informational documentation field); becomes NOT NULL when the platform flips to Class IIa under IEC 62304 §7.3 traceability — a one-line additive migration, which is why the column ships from day one rather than being retrofitted across thousands of catalog rows. Per D3 in exercise-taxonomy-pose-tracking.md.
Exercise reference code (EX-NNNN) [domain] The shareable "SKU" for a catalog exercise — EX-0042, displayed across Console / Clinic / Portal. Stored as exercises.reference_code, a STORED generated column over exercises.reference_number (a platform-global counter from a dedicated sequence); the display form is zero-padded to four digits but ungated (grows to EX-12345 past 9999 — no fixed ceiling). Distinct from the slug (the English, URL- and S3-key-safe identifier abdominal-psoas-isometric): the code is short and language-neutral so a patient can quote it ("my EX-0042 is broken") and a specialist can cite it in a program, regardless of UI language. Immutable once assigned (DB trigger) — patients and printed programs reference it, so it follows the same never-modify discipline as the slug. Platform-tier only: NULL for org-tier exercise rows; the EX- prefix denotes the platform catalog, and org-tier exercises (F9.2 Phase 3) get their own per-org code namespace when that tier ships. Searchable alongside the slug in the clinic library and the program-builder exercise picker. Deliberately not extended to renders or sessions/programs: renders are a regenerable cache keyed by role + language (use the derived EX-0042 · ro · instructions handle), and programs/sessions are per-clinic patient data with their own name/ordinal handles (a global code there would be a cross-tenant leak).
Feedback rule [domain] Per-pose-config rule that triggers a patient-facing message during exercise execution. Each rule carries a severity (warning | critical | stop), a condition_expression evaluated by the pose engine per frame, and a translatable patient_message (RO/EN at launch). Stored in exercise_pose_feedback_rules — the collapsed form of the mockup's two separate "form errors" and "live warnings" sections, kept as one table because they are semantically identical (condition → message) and differ only in severity. The bare schema ships without rate-limiting columns (min_interval_seconds, trigger_once_per_rep); those land as additive nullable columns when real "too noisy" complaints surface. Per D10 / D19 in exercise-taxonomy-pose-tracking.md.
Landmark subset [domain] The curated list of pose landmarks active for a specific exercise — typically 3–10 of the engine's full catalog (MediaPipe holistic exposes ~543 landmarks: 33 pose + 21 left hand + 21 right hand + 468 face). Stored as exercise_pose_landmarks join rows linking a Pose config to entries in the per-engine pose_landmarks reference catalog. Distinguished from the catalog itself: the Pose engine defines what landmarks exist; the subset declares which of those this exercise tracks. Fewer landmarks means more stable tracking and less CPU on the patient's device. Per D17 in exercise-taxonomy-pose-tracking.md.
Movement pattern [domain] Biomechanical classification of how the body moves during an exercise: push | pull | squat | hinge | rotation | lunge | carry | gait | hold (the last being isometric). Stored as exercise_movement_patterns (tag entity, M2M to exercise via exercise_tags — multi-pattern is expected, e.g., a squat is both squat and hinge). Platform-locked vocabulary — no org-private extension allowed (per D5) — because pose-engine rep-counting heuristics map directly to these values; org redefinition would break engine integration. Distinguished from body_region (the where axis) and recovery_phase (the when axis): movement_pattern is the how axis. Per D2 / D5 in exercise-taxonomy-pose-tracking.md.
Pose config [domain] [arch] Per-exercise configuration row (exercise_pose_configs) defining how the pose AI tracks execution: which Pose engine, what Landmark subset, what metrics (target / tolerance / weight / source landmarks / axis), what Feedback rules, what counts as a successful rep, plus camera angle / distance / lighting / in-frame requirements. 1:1 with exercise (per D8) — a different filming or camera setup is a different exercise, not a second config. Versioned via exercise_pose_config_history (immutable row snapshots) so historical session scoring stays reproducible. Pinned to exercises.asset_version: any re-filming auto-invalidates the config and reverts tracking_enabled to FALSE until a new config is authored (per D9). Class IIa columns (per D3) record who authored each detail row and on what clinical basis.
Pose data quality override [domain] [compliance] A specialist's clinical judgment that a session's pose data is unreliable and must be excluded from scoring — recorded in pose_data_quality_overrides with override_reason, overridden_by_principal_id, and a scope discriminator of either session_run or session_exercise_event (per B3). Raw pose data stays in the database (audit trail intact); the override flags it as not-counted for cohort stats and program promotion thresholds. Every insert is audit-logged with full principal_id and reason — Class IIa requirement. The table ships at F9.1 Phase 2; the Console UI for specialists to invoke the override is deferred (initial workflow = support request to platform team). Per B3 in exercise-taxonomy-pose-tracking.md.
Pose engine [domain] [arch] The underlying AI model that performs pose-landmark detection on the patient's video feed. Reference table pose_engines at the platform level (per D15), with rows like mediapipe.holistic, mediapipe.pose, and future engines. Each engine has its own landmark catalog (different vendors define different joint vocabularies), versioned via landmark_catalog_version so engine upgrades can add or deprecate landmarks without breaking historical pose configs. Distinguished from a Pose config: the engine is the model; the config picks which engine + landmarks + metrics this exercise uses. F9.1 Phase 2 seeds with mediapipe.holistic only. Per D15 in exercise-taxonomy-pose-tracking.md.
Recovery phase [domain] Clinical phase classification for exercise appropriateness during a patient's rehabilitation journey: acute | subacute | strength | return_to_activity | maintenance. Stored as exercise_recovery_phases (tag entity, M2M to exercise — exercises can span multiple phases, e.g., acute → strength). Platform-locked vocabulary (per D5) — comparable across clinics so cohort analytics remain meaningful; the enum is small enough that org-private extension would dilute rather than enrich it. Distinguished from difficulty (intrinsic hardness of the movement) and Skill prerequisite (what the patient can physically do): recovery_phase answers when in recovery this exercise is appropriate. Per D2 / D5 in exercise-taxonomy-pose-tracking.md.
Skill prerequisite [domain] A capability the patient must already have for an exercise to be safe and effective: balance_static | balance_dynamic | single_leg_stance | floor_to_stand | grip_strength | bilateral_coordination | weight_bearing_tolerance | core_endurance. Stored as exercise_skill_prerequisites (tag entity, M2M to exercise). Used by the program-builder for algorithmic safety gating — "don't prescribe Side Plank to a patient who can't hold Plank." Platform-locked vocabulary (per D5) because the gating logic is algorithmic. Distinguished from difficulty (a coarse intrinsic-hardness rating) and from exercise_prerequisites (a self-M2M chain between specific exercises, e.g., "Bird Dog before Side Plank") — skill_prerequisite is about patient capability, exercise_prerequisites is about exercise ordering. Per D2 / D5 in exercise-taxonomy-pose-tracking.md.
Compliance & data
GDPR-driven concepts. See decisions.md → Why clinic is controller, platform is processor.
Controller [compliance] Under GDPR, the entity that determines purposes and means of processing. The clinic is controller for patient health data. Patient data is the clinic's responsibility under their own privacy notice.
Processor [compliance] Under GDPR, an entity that processes data on the controller's documented instructions (DPA). The platform is processor. Cross-tenant features operate on anonymised data only; identifiable cross-tenant access goes through Break-glass.
DSAR [compliance] Data Subject Access Request. Routed through the clinic, never the platform. The Cross-Org Account Surface lets patients route their DSAR to each clinic they're at without the platform crossing the processor boundary.
Soft delete [compliance] [domain] Patient records are never hard-deleted. deleted_at column marks deletion; data is anonymized (PII stripped, structure preserved) per GDPR Art. 17(3)(c) medical-record exemption.
Anonymization [compliance] GDPR erasure as anonymization, not deletion — preserves clinical record structure for legal retention while removing identifying data.
RLS (Row-Level Security) [security] [tenancy] Postgres-level enforcement that every query is scoped to the current org context. The load-bearing isolation mechanism. See P1.
Audit log [compliance] [security] Append-only record of every state-changing mutation, plus failed authentication attempts. Range-partitioned monthly (audit_log table). Actor recorded as principal_id + denormalized actor_type. AI provenance columns populated for AI-driven actions. See P11.
Data classification [compliance] [security] Every column has a class (pii, phi, auth_secret, pii_regulated, non_pii, etc.) and a list of allowed egress targets. Default is block. CI fails if a new column is added without a registry entry. See data-classification.md.
Billing & metering
Concepts for charging clinics for platform usage.
Tier (org-side) [billing] A billable subscription product the platform sells to organizations. Lives in tiers table. Carries a base price + entitlements + quotas. Currently three: Free, Pro, Dedicated. Each org holds a current Tier via organization_subscriptions. Tier kind discriminates base (subscription tier) from addon (stackable upsell, foundation-deferred). Tier dedicated maps to tenancy_mode = 'dedicated'; tiers free and pro map to tenancy_mode = 'shared' (and shared is the only mode sellable end-to-end today — dedicated is a schema reservation pending the operational mechanisms). See Tenancy Mode. The earlier docs called this "Plan"; the rename to "Tier" unifies vocabulary with the patient side.
Tier (patient-side) [billing] A billable subscription product a clinic sells to its patients. Lives in patient_tiers table (per-org catalog, clinic-managed). Same engine shape as org-side Tier (entitlements, limits, snapshot-on-subscribe), differing in scope and audience. A clinic on either tenancy mode can offer any patient-side Tiers it wants — patient Tiers are orthogonal to org tenancy mode.
Tenancy Mode [arch] Architecture topology for an organization (organizations.tenancy_mode TEXT NOT NULL DEFAULT 'shared' CHECK IN ('shared', 'dedicated')). Two values: shared (default — pooled platform infrastructure with logical isolation via RLS + prefix scoping + app-layer entitlement checks) and dedicated (reserved — future mode with per-tenant Clerk org as the defining structural feature; optional addons like own-S3-bucket and own-CMK ship later as entitlements when their operational mechanisms exist, not as schema columns). Today only shared is provisionable via API — every creation path lands shared. Set at provisioning; effectively immutable in v1 (see tenant-isolation.md). The dedicated Tier maps to tenancy_mode = 'dedicated'.
Quota [billing] A cap on a metered capability (e.g., "max 1000 video minutes/month," "max 10 locations"), attached to Tiers via tier_limits → organization_subscription_limits and tracked live in usage_quotas.
Not a synonym for "blocks." limit_definitions.default_behavior decides that, and the choice is governed by who initiated the action: hard_block is admissible only where an administrator deliberately adds something (a Seat, a location, an integration); everything a patient or a clinician mid-care triggers is soft_meter and must never fail. A quota that refuses a booking, a form, a document, a reminder or a consultation is a defect regardless of its cap value. See tiers-and-subscriptions.md → What tiers differentiate on.
Seat [billing] One staff login at a clinic — an organization_memberships row held by a human principal, plus any staff invitation still outstanding. An issued invitation is a claimed seat: counting only memberships would let a clinic at its cap invite ten more people and discover the problem when each of them tried to sign in. Capped by max_staff_seats (Free 2 / Pro 10 / Dedicated unlimited), counted at the invitation endpoint because a lifetime limit can hold no usage_quotas row.
A Seat is not a Specialist row. A specialists row may be calendar-only (human_id IS NULL, no login) — how a clinic models treatment rooms, equipment, or a visiting practitioner who never signs in. Capping the roster would charge a clinic for describing its own premises, so max_specialists stays informational and seat enforcement counts memberships. See decisions.md → Why tier caps are on seats and features, not clinical volume.
Participant minute [billing] The unit every video provider bills in: one participant present for one minute. Two participants on a 30-minute consultation is 60 participant-minutes, not 30.
The platform records it twice, deliberately (F5.5). video.participant_minutes is the cost meter — actual minutes from join/leave pairs, what the provider invoices the platform, and nothing but the provider may supply it. video.billable_minutes is the billable meter — derived from completed appointments × scheduled duration × participants, predictable for the clinic and immune to a patient who joined an hour early. The two will disagree; the gap is visible in usage_records rather than invented at invoice time. Metered soft_meter, never hard_block: refusing a room because a monthly quota tripped means refusing a consultation a patient showed up for.
Other "plan"-words live in their feature docs
"Treatment Plan" (clinical-domain prescription document), and any other plan-shaped concept are feature-scoped, not cross-cutting. Their canonical definitions live in the relevant feature spec, not here. The cross-cutting commercial vocabulary is Tier (org-side), Tier (patient-side), Entitlement, Quota.
Usage Record [billing] [arch] An append-only event row capturing one metered capability call: (organization_id, capability, units, unit_type, cost_cents, principal_id, occurred_at, metadata). Range-partitioned monthly (event-shaped per P41).
Usage Summary [billing] Monthly aggregation per (org, capability) rolled from Usage Records by a cron, used for invoicing.
Invoice [billing] Generated from Usage Summaries + plan base price; charged via the payment provider (Stripe / FGO / Netopia depending on org). Lives in billing tables. Tax handling delegated to the invoicing provider — the platform never computes tax rates itself.
Marketplace mediation [billing] Strategic future product offering where the platform mediates patient → clinic payments end-to-end: patient pays platform; platform records balance owed to clinic; on payout schedule, platform pays clinic minus a platform fee. The standard pattern (Stripe Connect, Mollie marketplace, Netopia merchant-of-record) for SaaS marketplaces.
Distinct from the platform → clinic billing flow (which is foundation-accommodated and ships in F12 — clinic pays platform for plan + usage). Marketplace mediation is patient → clinic, with the platform as middleman.
Foundation accommodates marketplace mediation via four Cat A capability skeleton interfaces (payment.Provider, invoicing.Provider, patient_payment.Provider, clinic_payout.Provider) but does NOT build the engine. The marketplace engine is a separate strategic feature that ships when the company is ready (legal/regulatory review, fee model decision, KYB onboarding capacity). Many clinics will use Option A indefinitely (clinic-owned payment provider; platform never sees money) — Option B (marketplace mediation) coexists for clinics that want a turnkey solution. See foundation.md → Deferred Foundation Extensions.
Cross-cutting infrastructure
Patterns that every feature touches.
events.Bus [arch] Internal Go-process event bus (1A.9). Source of all Internal Events (Cat E). Subscribers fan out to audit, notifications, outbound webhooks, automations.
Capability framework [arch] Convention for declaring capabilities, registering implementation strategies, and attaching cross-cutting hooks (audit, metering, permission gating, error classification). Foundation primitive — every capability in the platform follows the same shape.
Provider resolution [arch] [integration] Foundation table (platform_service_providers) + helper that picks which Curated Provider impl handles a Cat A call for a given org. NULL organization_id rows = platform default; specific organization_id rows = per-org override (available on either tenancy mode). Capability interfaces always go through the resolver, even when only platform defaults exist.
Notification dispatcher [arch] Subscribes to events.Bus (Cat E), picks templates from the foundation registry, sends via channel adapters (EmailChannel, future SMSChannel, PushChannel) — each adapter is a Curated Provider (Cat A). See 1A.18.
Outbound webhook dispatcher [arch] [integration] Subscribes to events.Bus (Cat E), reads matching subscriptions from organization_integrations, signs payload, POSTs to clinic-configured URLs (Cat C). Independent code path from notifications — both can fire from the same domain event.
Inbound webhook framework [arch] [integration] Convention for /webhooks/{provider} route mounting + per-provider signature verification + state update + Internal Event emission. Cat D.
Metering middleware [arch] [billing] Wrapper around capability calls that writes a Usage Record per call. Hooked at the capability seam so every metered capability writes consistently.
Naming conventions
Already in CLAUDE.md → Naming Convention, restated for completeness:
- RestartiX = the brand/company. Never a specific app or service.
- Platform = the entire system (backend + frontends + infra).
- API service = the Go backend (
services/api/). The platform's primary, transactional API;media+telemetryare specialized satellite services. Resource/code/path name isapi(mirrors the directory). Formerly "Core API" / "desk API"; those service names are retired (the resource isapi). - Clinic app = staff frontend (
apps/clinic/). - Patient Portal = patient frontend (
apps/portal/). - Console = superadmin frontend (
apps/console/). - In code/paths:
api,clinic,portal,console,ui.
org_id vs organization_id — two names, deliberate
The platform uses both forms by layer; they are NOT synonyms to pick from at random.
| Layer | Form | Rationale |
|---|---|---|
| DB columns (and the DDL / RLS / indexes that name them) | organization_id | Foundation convention across every tenant-scoped table |
| Go struct fields | OrgID | Idiomatic Go; column-independent |
| JSON wire (JWT claims, API request/response bodies, signed-token payloads, OpenAPI schemas) | org_id | Short keys on the wire — tokens, query params, structured logs |
URL query params (?org_id=...) | org_id | Wire-form, matches JSON convention |
slog structured-log keys ("org_id", orgID) | org_id | Short keys; aligns with wire form |
S3 / Redis / cache key path templates ({org_id}/..., lock:{org_id}:...) | org_id | Path-form identifier, not a column reference |
The line is: if it names a DB column (DDL, indexes, RLS predicates, WHERE clauses, SQL fragments in docs, comments that say "the X column"), it's organization_id. If it names a wire field, log key, or path template, it's org_id. Prose mentions of "the org_id column on table X" are a column reference — use organization_id. Prose mentions of "the org_id claim in the JWT" are a wire reference — use org_id.
Existing drift was settled in two passes: API tables aligned at commit 6c42348 (programs / exercises / content_files renamed); the telemetry service's three tables (media_session_metrics, media_buffering_events, media_library_views) aligned in the follow-up commit. Both passes left wire-form org_id references alone on purpose.
legacy (the old product) vs handoff (the bridge mechanism)
Two distinct concepts that both said "legacy" before the 2026-06 rename:
| Concept | Name | Where it shows up |
|---|---|---|
The old product being replaced (restartix-admin-api / Strapi at platforma.restartix.ro) — the migration source | legacy | LEGACY_DATABASE_URL, legacy_id-style columns, "the legacy platform/side/app", "legacy self-migration" (migrating from legacy), MiniCRM/*_choices/legacy test-patient and other "old way" comments |
| The signed-token bridge mechanism that hands an already-signed-in patient from any external system into the platform (Strapi is just the first caller) — the how | handoff | core/handofftoken (the HS256 verifier, <purpose>token convention), domain/handoff (the verify→exchange→claim→reentry domain), Handoff* config fields, the handoff_* wire error codes, resolveHandoff/exchangeHandoffSession/claimHandoffMigration client methods, Handoff* API types |
The line: the source product is legacy; the bridge that carries you across is handoff. "Self-migration from the legacy platform" keeps "legacy" (it names the source); the token/verifier/domain/handler that performs it is handoff.
Step 1 of the rename (code-only, 2026-06) renamed the Go packages + identifiers + types + openapi + api-client, and deliberately left the wire surface on legacy: the URL paths (/v1/public/legacy/handoff, /v1/public/legacy/session, /v1/me/legacy/claim), the cookie names, and the LEGACY_HANDOFF_* env vars + restartix/{env}/legacy-handoff-secret SM path. Those move in step 2, in lockstep with the Strapi minter + the infra task-def/Secrets-Manager keys.
Two senses of prescription
Two unrelated clinical concepts said "prescription". The collision is real and must be settled before the F6 migration creates appointment_documents.
| Concept | Name | Where it shows up |
|---|---|---|
| A specialist assigning an exercise program to a patient — the shipped sense | prescription (bare) | protocols.kind = 'prescription' CHECK, uq_one_active_prescription_per_patient, the protocols.prescribe permission code, the content.prescription_play entitlement, protocols.KindPrescription in Go, the OpenAPI adherence-kind enum, the Portal paywall |
A rețetă medicală — a per-appointment PDF generated from a signed form and a frozen pdf_templates version (F6) | medical_prescription | the seeded document_categories key, carried on appointment_documents.category_key and offering_forms.category_key; the RO UI label "Rețete medicale" |
The rule: the shipped exercise-program sense keeps the bare word; the F6 document type is always qualified medical_prescription.
Which one gets qualified is not a coin flip. The exercise-program sense is live in production across a DB CHECK constraint, a unique index, a seeded permission code, an entitlement code, a Go constant, a wire enum and patient-facing copy — renaming it is a cross-surface break with a 000023 catch-up-DDL script attached, for zero gain. The document type has not been built, so qualifying it costs one enum value chosen before the migration is written. Prose that means the PDF says "medical prescription" or "rețetă medicală", never bare "prescription".
Two senses of report
The second collision of the same shape as prescription, surfaced 2026-08-08 while scoping clinic reporting.
| Concept | Name | Where it shows up |
|---|---|---|
A medical document for one patient — the raport medical, generated per appointment from a signed form against a frozen pdf_templates version (F6) | report (bare) | appointment_documents.category_key = 'report', offering_forms.category_key = 'report', the RO UI label "Rapoarte" |
| Clinic-level reporting — aggregate figures, funnels and trends over many patients | analytics | The shipped /analytics page, the stats Go domain (HandleOrgFunnel, HandleOrgRuns, HandleOrgPainEvents), F16 |
The rule: the per-patient document keeps the bare word; clinic-level reporting is always analytics.
Same reasoning as prescription. The document sense is the one with tenure — a DB CHECK value, a form slot, a UI label, and a whole F6 feature built around it — while clinic reporting has an existing surface already named /analytics and a Go domain already called stats. Nothing needs renaming; what needs preventing is a future "Reports" section of the clinic app that means analytics and collides with Raport medical in the same navigation. Prose that means the aggregate sense says "analytics", never "reports".
Clinical measurement
Measure
A named, typed quantity observed about a patient at a point in time — (patient, measure_key, value, observed_at, source). Introduced by F16 Longitudinal Measures.
A measure series is every point for one patient and one measure key, in order. It is derived at read time, never stored — there is no patient_measurements table, matching F9.4's existing "query-time aggregation against raw event tables" rule, and avoiding a second truth for form answers whose whole purpose is immutability.
Three sources supply points: session_pain_events, session_runs (pain / RPE), and forms.values answers on scale / number questions. custom_field_values cannot — it holds one row per (org, entity, field), overwritten in place, so it has no value history. That is F3's three-layer model working as designed: custom_field_values is what is true now, forms.values is what was said then, and a measure series is the latter.
Measure key is custom_fields.key when the form question binds to the field library (so the same clinical quantity asked on two templates is ONE series), otherwise template-local. The binding is already materialised into forms.fields, the frozen snapshot, so series identity is retroactively present in existing data and needs no migration.
A F15 filter predicate is the last point of a measure series — the same model queried at one point instead of across all of them.
Measure vs. Instrument
Instrument is the clinical-assessment sense already in use around session_runs (a VAS scale, an RPE scale — the tool that produces a reading). Measure is the platform's read-side abstraction over readings from any source, including instruments. An instrument produces measures; a measure does not imply an instrument, since a plain number question on a form produces one too.
Forbidden terms
Any usage of these terms in new code, new docs, or commit messages is a rename target. Existing usages get cleaned up via mechanical sweeps.
| Forbidden | Use instead | Why |
|---|---|---|
desk / "Desk API" / core-api / "Core API" | api ("API service") | Old service names; the Go backend's resource/code name is api (mirrors services/api/). core-api was an interim rename from desk, retired once media + telemetry made the "api" suffix ambiguous. |
users (table or type) | principals | The actor model is principal-rooted; users doesn't exist |
user_id (in new schema) | principal_id (when any actor) or human_id (when human-required) | FK target enforces actor-type constraint |
services (clinical-domain) | offerings | Architectural-vs-domain word collision. Deferral condition met 2026-08-02 — the F2.1 offerings stand-in is in scope, so the table is created as offerings from day one and the doc/spec sweep runs with that build. See Rename status. |
service_plans / patient_service_plans (clinical-domain) | offering_packages | enrollmentsenrollment is taken by the shipped protocols.kind = 'enrollment' + the protocols.enroll permission code (live in production), where it means "patient self-enrolled in a guided program." offering_packages collides with nothing and is domain-prefixed like offering_specialists / offering_forms. Concept itself stays deferred (F2.2) — the row reserves a name, it does not schedule a build. |
prescription as the name of the F6 PDF document type | medical_prescription | Bare prescription is the shipped exercise-program sense (protocols.kind, protocols.prescribe, content.prescription_play). See Two senses of prescription. Settle before the appointment_documents migration. |
services (Cat A naming in code/docs) | "Curated Providers" or just "providers" | Avoid colliding with the clinical Offering domain. |
| "Reports" as the name of a clinic-level reporting / aggregate surface (UI section, route, or domain) | analytics | Bare report is the per-patient medical document — now a seeded document category (system_key = 'report', RO label "Rapoarte") carried on appointment_documents.category_key and offering_forms.category_key. Clinic-level reporting already has a surface at /analytics and a Go domain called stats. See Two senses of report. |
features (DB table for entitlement catalog) | entitlements | Architectural "feature" means user-facing functionality, not billable gate. Rename completed 2026-05-06 (foundation 1C.9). |
plan_features (plan→feature mapping) | tier_entitlements | Rename follows from features → entitlements. Rename completed 2026-05-06 (foundation 1C.9). |
organization_capabilities (per-org gates) | organization_entitlements | Architectural "capability" means internal interface, not org gate. Same rename family. Rename completed 2026-05-06 (foundation 1C.9). |
| Bare "plan" or "tier" without context | qualify with billing/treatment/patient/etc. | "Plan" alone collides across Tier-vs-Plan, Treatment Plan, and (future) Patient Subscription Plan. Always qualify. |
current_app_user_id() | current_app_principal_id() | RLS helpers reference principals, not users |
org_id as a DB column name (DDL, indexes, RLS, SQL examples) | organization_id | Foundation convention across every tenant-scoped table. Wire-form (org_id in JWT claims, log keys, query params, S3 paths) stays — see Naming conventions |
legacyhandoff / domain/legacy / LegacyHandoff* / LegacyClaim* (as the bridge mechanism) | handofftoken / domain/handoff / Handoff* | The signed-token bridge is handoff (the how); legacy is reserved for the old product (the migration source). See legacy vs handoff. Code rename done 2026-06 (step 1); wire surface — URL paths, cookies, LEGACY_HANDOFF_* env — moves in step 2. |
| Mixing Cat A/B/C/D as one "integration" | Use the specific category | Confusing them caused historic design drift |
form_templates.type / offering_forms.slot / calendar_forms.slot / pdf_templates.template_type / appointment_documents.type (as a closed enum) | category_id (FK) or category_key (denormalised), against document_categories | The seven-value vocabulary was a CHECK constraint asserting which kinds of paperwork a clinic may have — a claim the platform is in no position to make, and one that left report unassignable. See Document category. |
AttachableSlots / MultipleSlots / slotOrder / singleCardinalitySlots (hardcoded lists in Go) | document_categories.filled_by / .cardinality / .sort_order | Each encoded one property of a category as a constant. A clinic adding a kind of paperwork needed a migration and a release for a word. |
appointment_types | calendars | Platform-internal losing name for the booking-configuration entity. Still present in older scheduling / appointments / integrations feature docs and the features/scheduling/go/* reference files; rename before anyone copies it into a migration. |
"profile sharing" / profile_shared / profile_sharing (as a disclosure gate on the portable patient profile) | "the patient is registered at this clinic" | Removed 2026-08-20. There is no per-field gate: a clinic reads the portable profile because it holds a patients row against it, and registering IS the disclosure. Prefilling a patient's own date of birth so they need not retype it is convenience, not a second act of sharing — and consent was the wrong legal basis for treatment data (Art. 9(2)(h), not 9(2)(a)). See P8 — retired. |
leo-legacy vocabulary
F1–F6 are ports of the live restartix-leo-* + restartix-intakes system. Its vocabulary does not come across — every term below has a platform equivalent, and carrying the old word into a migration, a Go identifier, or a UI string is a rename target on sight. Full evidence in leo-port-map.md §5 (C16).
| Forbidden (leo) | Use instead | Why |
|---|---|---|
franchise / franchises | organization / organizations | leo's tenant entity. The platform's tenant boundary is the Organization, and organization_id NOT NULL + RLS is the hard rule on every tenant table — leo's franchise scoping is app-layer middleware applied to list-GETs only. |
Opening / openings / opening_weekly_hours (leo/Intakes) | Specialist + its availability tables (specialist_weekly_hours, specialist_schedule_overrides) | In Intakes an "opening" is a specialist's bookable presence, provisioned lazily and separately from the leo specialist row — the split is precisely what makes leo's specialists silently unbookable. One entity on the platform: the Specialist owns its own availability. |
Intake / intakes (the booking row) | Appointment / appointments | The booked row lives in a second service in leo. On the platform there is one appointments table in one database — see port map C10. |
Schedule / schedules (leo/Intakes booking config) | Calendar / calendars | Slot duration/gap, cooldown, lead time, horizon, assignment strategy. "Schedule" is too generic and collides with the cron/events.Bus scheduler; calendars wins. |
appointment_template | offerings (+ calendars for the booking half) | leo's single entity does three jobs — catalog identity, booking configuration, and form-slot/roster binding. The platform splits it: identity + roster + form slots in offerings (F2.1), booking configuration in calendars (F4). |
meta_field / meta_value | custom_fields / custom_field_values (+ system_key for platform-defined fields) | "Meta" says nothing about what the row is. Also a scoping fix: meta_field.key is globally unique in leo; the platform's is UNIQUE (organization_id, entity_type, key). |
speciality / specialities | specialty / specialties | Misspelling, straight through leo's schema, routes and UI. Do not carry it into a table, column, JSON key or URL. |
Stewardship
This doc is updated whenever a new term is introduced or an existing term gains/loses a meaning. PRs that introduce new vocabulary without a glossary entry are incomplete. The CI hook (or pre-merge review) for new docs/migrations should grep for unrecognized terms against this glossary.
When a forbidden term appears in a PR, the rule is: rename in this PR or open a tracked rename issue. Don't paper over.