Skip to content

Data Classification

Every column the platform stores carries a class (what kind of data it is) and a list of egress targets (where it is allowed to flow outside the tenant). The registry below is the source of truth, enforced by:

  • CI checkservices/api/cmd/check-classification parses every migrations/core/*.up.sql and this file. Build fails if a schema column is missing from the registry, the registry references a non-existent column, or a class/target name is undefined. Wired into make check and the GitHub Actions PR pipeline.
  • Runtime helperservices/api/internal/shared/classification/ parses this doc once at startup. Egress paths call classification.AllowedFor(table, target) []string and classification.Filter(record, target) any to project allowed columns. Default is block: a column missing from the registry, or with no matching egress target, cannot leave the tenant.

The plan that put this in place is implementation-plan.md → Layer 1.25. The rationale is in decisions.md → Why a column-level data classification.


Class taxonomy

Each class implies retention, encryption, RLS, and audit expectations. Adding a class is a deliberate change to this doc + the runtime helper's enum, not casual.

ClassDefinitionImplications
publicNo protection required. Org public face (name, slug, logo URL), platform catalog (plan codes, feature names, permission codes).No encryption. No RLS scoping needed (catalog tables have permissive SELECT policies). May appear in unauthenticated responses.
org_internalSettings, configuration, internal flags. Visible to org members but not public.RLS-scoped to current_app_org_id(). Plaintext fine. Must not leak across orgs.
pii_basicNames, emails, phones, addresses, normal contact info. Identifies a person but is not a regulated identifier or a credential.Plaintext at rest. RLS-scoped. Mask in logs (P11). Subject to GDPR access/erasure. Protection is the layered envelope (RLS + audit + at-rest disk encryption + encrypted backups + restricted DB access), not column-level encryption. See decisions.md → Why most PII is plaintext.
pii_regulatedNational IDs, SSNs, tax IDs (CUI in RO), passport numbers — extra-protected by national law beyond GDPR Art. 6.Column-encrypted at the application layer (P12). RLS-scoped, plus typically a per-row read audit. Mask in logs. Column name MUST end in _encrypted and type MUST be BYTEA — enforced by cmd/check-classification.
clinicalDiagnoses, treatments, notes, prescriptions — health data under GDPR Art. 9.RLS-scoped. Plaintext fine at rest under EU MDR/GDPR for clinical use. Audit reads at the row level. Soft-delete only (P13).
clinical_sensitiveMental health, sexual health, HIV status, addiction, genetic data — GDPR Art. 9 special category with the strictest handling.Same as clinical plus: per-row read audit always (no batch summaries), explicit consent at write, surfaced through dedicated UI surfaces only.
auth_secretCredentials and authentication artifacts: external auth-provider subject IDs (cross-system identifier — Clerk JWT sub today, any future provider's equivalent), API key hashes, webhook signing secrets, OAuth refresh tokens, domain verification tokens. Compromise = identity takeover.Never logged (mask absolutely). Hashed where the wire format is a credential (API keys are SHA-256 BYTEA); column-encrypted where the platform must read the value back (signing secrets, refresh tokens). Cross-system identifiers (e.g., humans.provider_subject_id) and short-lived verification tokens may stay TEXT — cmd/check-classification only requires BYTEA on *_encrypted and *_hash columns. Excluded from every egress target by default.
audit_onlyIPs, user agents, request paths, audit-row metadata. Pseudonymous PII per GDPR — useful for security/compliance, never for product features.Stored in audit_log. RLS gated on audit_log.view_org permission. Retention ≥ 6 years (CLAUDE.md).
system_metadataTimestamps, foreign keys, internal IDs that carry no user-facing meaning on their own.No special handling. May still flow only to targets that explicitly allow it; system_metadata is not a "go anywhere" pass.

Egress target taxonomy

A target is an external surface where data leaves the tenant. The registry's egress column lists the targets each column is allowed for. Targets extend per-feature — adding a target is a deliberate change to this doc + the runtime helper's enum.

TargetWhere it appliesNotes
bulk_exportGDPR Art. 20 patient data portability — the patient downloads a structured archive of their own data.Recipients are end users. Lights up when the GDPR export endpoint ships (deferred to a Layer 12 or post-Layer 8 feature).
analytics_internalTelemetry service pipeline. Pseudonymized identifiers only — the pseudonymization helper (internal/shared/pseudonym/) is applied separately at the egress site.Recipients are platform staff via dashboards. The registry permits the column to leave; pseudonymization is a transform, not a registry decision.
webhook_egressOutbound webhooks (Layer 8) for clinic-installed integrations. Per-event payloads.Per-org subscription; org-controlled.
marketing_emailLayer 8 marketing campaigns and transactional notifications that include user-identifying content.Strict per-patient consent gate (P17) on top of the registry.
support_exportBreak-glass support exports — when platform support staff legitimately need to dump org or principal data to investigate an incident.Audited as action_context = 'support_export'. Excludes credentials by class — auth_secret columns never appear here.
ai_clinical_drafting(Placeholder.) The first AI feature drafting clinical content. Per-clinic consent for the AI processing purpose (P17) on top of the registry.Empty across the registry until the first AI clinical feature ships.
ai_admin_summarization(Placeholder.) The first AI feature summarizing admin/operational data. Per-clinic consent on top of the registry.Empty across the registry until the first such feature ships.
patient_documentThe F6 PDF renderer — a generated report or medical prescription, composed in the Clinic app and handed to the patient.Distinct from bulk_export: a portability archive is the patient's own data going to the patient, whereas a document carries what the clinic may see, which its own patients row decides. national_id_encrypted reaches a document only when a published patient_details block selected the cnp field. Enforced at BUILD time, not at render time — see below.

How callers use it

internal/shared/classification exposes the intended runtime shape — an egress path consults the helper before constructing a payload, and never hand-builds the field list:

go
// Allowed column names for that table+target. Empty slice = nothing leaves.
cols := classification.AllowedFor("organizations", "support_export")

// Project a record to only the allowed columns. Reflection-based; works on
// tagged structs and map[string]any. Unknown table/target = empty result.
filtered := classification.Filter(record, "support_export")

Read this next part before reasoning about what the registry enforces today.

Load parses this markdown file, and nothing calls it at runtime. The API image is built from the services/api directory alone, so this document is not in the container at all; the only caller is cmd/check-classification, at build time. That makes the registry today a build-time contract, not a runtime filter — the guarantee is "code and registry agree when the build passes", not "the process consults the registry per request".

That distinction is load-bearing, and getting it wrong has already cost something: this section previously claimed the helper "parses this doc once at process startup", which made the registry read as an enforcing control. Under that belief, humans.email sat classified support_export-only while the F6 renderer printed it onto patient documents for the whole life of the feature. Nothing caught it, because nothing was checking (found and corrected 2026-08-07).

So each target is enforced by a specific, nameable mechanism, and the honest answer per target is:

TargetEnforced by
patient_documentCI. check-classification asserts every field a patient_details block can print maps to a column whose row here allows patient_document. Adding a printable field without a registry row — or with a row that forbids it — fails make check.
everything elseNothing yet. No egress path calls AllowedFor at runtime. bulk_export lights up with F11's DSAR export, which is where a runtime registry genuinely earns its keep: there the field list is data rather than a fixed catalog, and a CI assertion cannot cover it.

A runtime registry — a generated Go table compiled into the binary, kept in sync with this file by CI — is the shape that closes the rest. It was deliberately not built for A2: while a printable field list is fixed and enumerable, the build-time assertion gives the same protection for a fraction of the machinery. Build it with the first egress path whose field list is genuinely dynamic.


Encryption invariants

Column-level encryption is reserved for two narrow categories: credential material (auth_secret) and regulated identifiers (pii_regulated). Every other class — including pii_basic, clinical, and clinical_sensitive — is plaintext at rest, protected by the layered envelope (RLS + audit + at-rest disk encryption + encrypted backups + restricted DB access). The rationale is in decisions.md → Why most PII is plaintext.

services/api/cmd/check-classification enforces three structural invariants on every make check run, so drift in either direction fails the build:

  1. Regulated identifiers must be encrypted. A column classified pii_regulated MUST have type BYTEA AND a name ending in _encrypted. Catches the case where someone adds passport_number TEXT and classifies it pii_regulated — the build fails until the column is renamed and re-typed (or the class is downgraded with documented reasoning).
  2. The _encrypted suffix is reserved. A column whose name ends in _encrypted MUST have type BYTEA AND class pii_regulated or auth_secret. Catches the case where someone adds address_encrypted BYTEA classified pii_basic — the build fails until the column is renamed (matching the plaintext rule for pii_basic) or the class is upgraded.
  3. Credential hashes are BYTEA. A column whose name ends in _hash AND class is auth_secret MUST have type BYTEA. Catches api_key_hash TEXT declared auth_secret — SHA-256 belongs in BYTEA, not hex-encoded text.

What's intentionally NOT enforced:

  • auth_secret columns aren't required to be BYTEA across the board. Cross-system identifiers like humans.provider_subject_id and short-lived domain_verification_tokens.token are TEXT today and the wire format is opaque to us; the invariants only fire on the encryption-style suffixes.
  • pii_basic, clinical, clinical_sensitive, org_internal, audit_only, system_metadata, and public columns have no encryption-related constraints. They rely on the layered controls.
  • Columns whose name happens to end in _hash outside an auth_secret context — e.g., audit_log.inputs_hash (system_metadata, a SHA-256 forensic linker) — are not constrained. The invariant only applies when class is auth_secret.

When adding a new column, the class drives the encryption posture automatically. Pick the class; the invariants pick the column shape.


Adding a new column

When a migration adds a new column:

  1. Decide its class from the table above.
  2. Decide which egress targets it is explicitly allowed for. Default is none.
  3. Add a row to the appropriate registry section below — same PR as the migration. CI rejects PRs that add a column without a registry entry.

When a migration renames a column, update the registry row in the same PR. The CI check fails on dangling registry rows referring to columns that no longer exist.

When a migration drops a column, drop the registry row in the same PR.


Registry

One row per (table, column). Columns with no egress entry have an empty cell and are blocked from every target by default. Tables are grouped by data-model area; ordering within a section follows column-declaration order in the migration for reviewability.

Audit & provenance

audit_log

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
actor_idsystem_metadatasupport_export
actor_typeaudit_onlysupport_export
actionaudit_onlysupport_export
entity_typeaudit_onlysupport_export
entity_idaudit_onlysupport_export
changesaudit_onlysupport_export
ip_addressaudit_onlysupport_export
user_agentaudit_onlysupport_export
request_pathaudit_onlysupport_export
request_methodaudit_onlysupport_export
status_codeaudit_onlysupport_export
request_idaudit_onlysupport_export
action_contextaudit_onlysupport_export
break_glass_idaudit_onlysupport_export
impersonation_idaudit_onlysupport_export
patient_profile_idaudit_onlysupport_export
created_atsystem_metadatasupport_export

patient_profile_id is WHO A ROW IS ABOUT, as opposed to actor_id (who did it). audit_only like the rest of the event metadata: it is a pointer, not a personal detail — it identifies a person only to somebody who can already resolve patient_profiles, which is exactly the audience audit_select admits. Declared with the table in 000001. NULL on every row that concerns no patient — role grants, org settings, catalog edits — which is most of them.

audit_ai_provenance

ColumnClassEgress
audit_log_idsystem_metadatasupport_export
audit_log_created_atsystem_metadatasupport_export
model_idaudit_onlysupport_export
inputs_hashaudit_onlysupport_export
confidenceaudit_onlysupport_export
created_atsystem_metadatasupport_export

Identity

principals

ColumnClassEgress
idsystem_metadatasupport_export
principal_typesystem_metadatasupport_export
parent_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

humans

ColumnClassEgress
principal_idsystem_metadatasupport_export
provider_subject_idauth_secret
provider_org_idauth_secret
emailpii_basicsupport_export, patient_document
namepii_basicsupport_export, patient_document
confirmedorg_internalsupport_export
blockedorg_internalsupport_export
portal_credential_generationauth_secret
last_activityaudit_onlysupport_export
preferred_languageorg_internalsupport_export
timezoneorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

email_change_requests

A pending change of the address on an account. Both addresses are pii_basic for the same reason humans.email is — they are the same value, one before and one after. token_hash is auth_secret with no egress at all: it authorises re-pointing a login, which is the highest-value thing a token in this platform can do.

ColumnClassEgress
idsystem_metadatasupport_export
human_idsystem_metadatasupport_export
new_emailpii_basicsupport_export
old_emailpii_basicsupport_export
token_hashauth_secret
expires_atsystem_metadatasupport_export
confirmed_atsystem_metadatasupport_export
cancelled_atsystem_metadatasupport_export
requested_by_principal_idsystem_metadatasupport_export
initiated_bysystem_metadatasupport_export
organization_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

agents

ColumnClassEgress
principal_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
descriptionorg_internalsupport_export
model_providerorg_internalsupport_export
model_nameorg_internalsupport_export
model_versionorg_internalsupport_export
scopeorg_internalsupport_export
system_prompt_reforg_internalsupport_export
configurationorg_internalsupport_export
enabledorg_internalsupport_export
deleted_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

service_accounts

ColumnClassEgress
principal_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
descriptionorg_internalsupport_export
integration_kindorg_internalsupport_export
api_key_hashauth_secret
api_key_prefixorg_internalsupport_export
expires_atorg_internalsupport_export
last_used_ataudit_onlysupport_export
rotated_atorg_internalsupport_export
revoked_atorg_internalsupport_export
deleted_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

platform_memberships

ColumnClassEgress
principal_idsystem_metadatasupport_export
roleorg_internalsupport_export
granted_by_principal_idsystem_metadatasupport_export
granted_atsystem_metadatasupport_export

RBAC

permissions

ColumnClassEgress
codepublicsupport_export
resourcepublicsupport_export
actionpublicsupport_export
descriptionpublicsupport_export
created_atsystem_metadatasupport_export

roles

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
codeorg_internalsupport_export
nameorg_internalsupport_export
descriptionorg_internalsupport_export
is_systemorg_internalsupport_export
customized_atsystem_metadatasupport_export
requires_specialist_profileorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

role_permissions

ColumnClassEgress
role_idsystem_metadatasupport_export
permission_codesystem_metadatasupport_export
created_atsystem_metadatasupport_export

organization_memberships

ColumnClassEgress
principal_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
role_idsystem_metadatasupport_export
is_ownersystem_metadatasupport_export
last_used_ataudit_onlysupport_export
invited_atsystem_metadatasupport_export
invited_bysystem_metadatasupport_export
accepted_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Organizations & domains

organizations

ColumnClassEgress
idsystem_metadatasupport_export
namepublicsupport_export, bulk_export
slugpublicsupport_export
taglinepublicsupport_export
descriptionpublicsupport_export
emailpublicsupport_export
phonepublicsupport_export
websitepublicsupport_export
locationpublicsupport_export
logo_urlpublicsupport_export
icon_urlpublicsupport_export
language_codeorg_internalsupport_export
portal_self_signup_enabledpublicsupport_export
brandingpublicsupport_export
tenancy_modeorg_internalsupport_export
activated_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

organization_domains

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
domainorg_internalsupport_export
domain_typeorg_internalsupport_export
statusorg_internalsupport_export
cloudflare_hostname_idsystem_metadatasupport_export
ssl_statusorg_internalsupport_export
verification_tokenauth_secret
verified_atsystem_metadatasupport_export
last_check_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Org settings & companions

organization_settings

ColumnClassEgress
organization_idsystem_metadatasupport_export
marketing_email_enabledorg_internalsupport_export
marketing_sms_enabledorg_internalsupport_export
audit_retention_monthsorg_internalsupport_export
support_localeorg_internalsupport_export
default_timezoneorg_internalsupport_export
feature_flagsorg_internalsupport_export
late_cancellation_hoursorg_internalsupport_export
noshow_grace_minutesorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

organization_billing

ColumnClassEgress
organization_idsystem_metadatasupport_export
current_tier_idorg_internalsupport_export
billing_emailpii_basicsupport_export
billing_contact_namepii_basicsupport_export
billing_address_line1pii_basicsupport_export
billing_address_line2pii_basicsupport_export
billing_citypii_basicsupport_export
billing_postal_codepii_basicsupport_export
billing_countrypii_basicsupport_export
tax_id_encryptedpii_regulatedsupport_export
currencyorg_internalsupport_export
external_customer_idorg_internalsupport_export
payment_providerorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

organization_entitlements

ColumnClassEgress
organization_idsystem_metadatasupport_export
telerehab_enabledorg_internalsupport_export
video_consultations_enabledorg_internalsupport_export
pose_estimation_enabledorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

organization_designations

Per-org legal/regulatory contact assignments (DPO, billing contact, etc.). External-contact fields are PII when the designee is an external party (a contracted DPO firm's named person + email + phone); the same shape carries no PII when the designation points at an internal principal_id. Classification is the upper bound, applied to the columns regardless of which case populates them.

ColumnClassEgress
organization_idsystem_metadatasupport_export
kindsystem_metadatasupport_export
principal_idsystem_metadatasupport_export
external_contact_namepii_basicsupport_export
external_contact_emailpii_basicsupport_export
external_contact_phonepii_basicsupport_export
notesorg_internalsupport_export
assigned_by_principal_idsystem_metadatasupport_export
assigned_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

organization_ownership_transfers

State machine for org ownership handoffs. accept_token is generated, used once, and presented by the recipient to claim ownership — same secret-class as auth credentials. Notes are operator/operator-supplied free text scoped to the org's directory.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
from_principal_idsystem_metadatasupport_export
to_principal_idsystem_metadatasupport_export
initiated_by_principal_idsystem_metadatasupport_export
accept_tokenauth_secret
statussystem_metadatasupport_export
initiated_atsystem_metadatasupport_export
expires_atsystem_metadatasupport_export
resolved_atsystem_metadatasupport_export
initiation_noteorg_internalsupport_export
resolution_noteorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Tiers catalog

tiers

ColumnClassEgress
idpublicsupport_export
codepublicsupport_export
namepublicsupport_export
descriptionpublicsupport_export
kindpublicsupport_export
billing_cyclepublicsupport_export
base_pricepublicsupport_export
currencypublicsupport_export
versionpublicsupport_export
publishedpublicsupport_export
is_publicpublicsupport_export
published_atpublicsupport_export
deprecated_atpublicsupport_export
translationspublicsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

tier_versions

ColumnClassEgress
idsystem_metadatasupport_export
tier_idsystem_metadatasupport_export
versionpublicsupport_export
published_atpublicsupport_export
entitlements_snapshotpublicsupport_export
limits_snapshotpublicsupport_export
metadata_snapshotpublicsupport_export
changed_by_principal_idorg_internalsupport_export
created_atsystem_metadatasupport_export

entitlements

ColumnClassEgress
codepublicsupport_export
namepublicsupport_export
descriptionpublicsupport_export
regulatedpublicsupport_export
scopepublicsupport_export
entitlement_columnorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

limit_definitions

ColumnClassEgress
codepublicsupport_export
namepublicsupport_export
descriptionpublicsupport_export
unitpublicsupport_export
default_behaviorpublicsupport_export
period_kindpublicsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

tier_entitlements

ColumnClassEgress
tier_idpublicsupport_export
entitlement_codepublicsupport_export
enabledpublicsupport_export
created_atsystem_metadatasupport_export

tier_limits

ColumnClassEgress
tier_idpublicsupport_export
limit_codepublicsupport_export
cap_valuepublicsupport_export
behavior_overridepublicsupport_export
created_atsystem_metadatasupport_export

Org subscriptions

organization_subscriptions

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
tier_idorg_internalsupport_export
tier_versionorg_internalsupport_export
statusorg_internalsupport_export
started_atorg_internalsupport_export
current_period_starts_atorg_internalsupport_export
current_period_ends_atorg_internalsupport_export
cancel_atorg_internalsupport_export
canceled_atorg_internalsupport_export
payment_providerorg_internalsupport_export
external_subscription_idorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

organization_subscription_entitlements

ColumnClassEgress
subscription_idsystem_metadatasupport_export
entitlement_codeorg_internalsupport_export
enabledorg_internalsupport_export
created_atsystem_metadatasupport_export

organization_subscription_limits

ColumnClassEgress
subscription_idsystem_metadatasupport_export
limit_codeorg_internalsupport_export
cap_valueorg_internalsupport_export
behaviororg_internalsupport_export
created_atsystem_metadatasupport_export

organization_subscription_overrides

ColumnClassEgress
idsystem_metadatasupport_export
subscription_idsystem_metadatasupport_export
override_kindorg_internalsupport_export
entitlement_codeorg_internalsupport_export
entitlement_enabledorg_internalsupport_export
limit_codeorg_internalsupport_export
cap_valueorg_internalsupport_export
behavior_overrideorg_internalsupport_export
granted_by_principal_idorg_internalsupport_export
reasonorg_internalsupport_export
effective_fromorg_internalsupport_export
expires_atorg_internalsupport_export
revoked_atorg_internalsupport_export
revoked_by_principal_idorg_internalsupport_export
created_atsystem_metadatasupport_export

Patient tiers

patient_tiers

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
codeorg_internalsupport_export
nameorg_internalsupport_export, bulk_export
descriptionorg_internalsupport_export
is_activeorg_internalsupport_export
is_defaultorg_internalsupport_export
sort_orderorg_internalsupport_export
versionorg_internalsupport_export
publishedorg_internalsupport_export
published_atorg_internalsupport_export
external_price_hintorg_internalsupport_export
currencyorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

patient_tier_versions

ColumnClassEgress
idsystem_metadatasupport_export
tier_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
versionorg_internalsupport_export
published_atorg_internalsupport_export
entitlements_snapshotorg_internalsupport_export
limits_snapshotorg_internalsupport_export
metadata_snapshotorg_internalsupport_export
changed_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export

patient_tier_entitlements

ColumnClassEgress
tier_idsystem_metadatasupport_export
entitlement_codeorg_internalsupport_export
enabledorg_internalsupport_export
created_atsystem_metadatasupport_export

patient_tier_limits

ColumnClassEgress
tier_idsystem_metadatasupport_export
limit_codeorg_internalsupport_export
cap_valueorg_internalsupport_export
behavior_overrideorg_internalsupport_export
created_atsystem_metadatasupport_export

Patient identity

patient_profiles

ColumnClassEgress
idsystem_metadatasupport_export, bulk_export
human_idsystem_metadatasupport_export, bulk_export
namepii_basicsupport_export, patient_document, bulk_export
date_of_birthpii_basicsupport_export, patient_document, bulk_export
sexpii_basicsupport_export, patient_document, bulk_export
phonepii_basicsupport_export, patient_document, bulk_export
occupationpii_basicsupport_export, patient_document, bulk_export
residencepii_basicsupport_export, patient_document, bulk_export
national_id_encryptedpii_regulatedbulk_export, patient_document
national_id_hmacsystem_metadata
blood_typeclinicalsupport_export, patient_document, bulk_export
allergiesclinicalsupport_export, patient_document, bulk_export
chronic_conditionsclinicalsupport_export, patient_document, bulk_export
emergency_contact_namepii_basicsupport_export, patient_document, bulk_export
emergency_contact_phonepii_basicsupport_export, patient_document, bulk_export
insurance_entriespii_basicsupport_export, patient_document, bulk_export
created_atsystem_metadatasupport_export, bulk_export
updated_atsystem_metadatasupport_export, bulk_export
anonymized_atsystem_metadatasupport_export, bulk_export

Seven of these gained patient_document on 2026-08-07 (A2): sex, blood_type, allergies, chronic_conditions, both emergency-contact columns and insurance_entries. They had been support_export-only, not because printing them was weighed and refused but because the document catalog was inherited from the legacy system's report and never revisited — a patient could fill these in through a form and no document could print them back. allergies and chronic_conditions are clinical, and that is the point of them being on a medical report; the printing clinic's own patients row still scopes them, exactly as it does a date of birth.

national_id_hmac carries no egress target at all, which is the registry's way of saying it may never leave the tenant by any path. It is a blind index — HMAC-SHA256 of the CNP under a key derived from the active encryption key — and it exists so a clinic can look a patient up by CNP, which random-nonce AES-GCM makes impossible. It is classified system_metadata rather than pii_regulated because the encryption invariants reserve the _encrypted suffix for values that decrypt back, and this one never does; the empty target list is what actually protects it.

It leaks EQUALITY by design: two rows with the same digest hold the same CNP. That is how the lookup works and how a duplicate patient record surfaces. It leaks nothing else without the derived key — but an attacker holding both the database and that key can confirm a guessed CNP offline, so it widens the blast radius of a key compromise and nothing else.

The 2026-08-21 bulk_export amendment (Art. 15 / Art. 20)

bulk_export is defined in this document as the PATIENT'S GDPR ARCHIVE — see the pose_data_quality_overrides and session_pose_overrides sections, both of which grant it so "the patient's GDPR archive reflects the override." The target existed, was documented, and had no caller; the rows behind it were never populated for the tables an archive is actually made of.

The state before this amendment was not a policy, it was a gap, and it was incoherent on its face: the ONLY patient_profiles column permitted to reach a patient's own archive was national_id_encrypted, so an export could return someone their encrypted CNP and not their name. forms.values and forms.files — the answers the patient themselves typed — carried NO egress target at all, meaning the platform would refuse to give a person back the health information they had personally entered. Art. 15(3) requires exactly that copy, and Art. 20 requires it machine-readable.

THE RULE APPLIED, and the one to apply to any future row: a column reaches bulk_export when it is the patient's own data about themselves.

Excluded deliberately, and consistently with what this registry already does elsewhere:

  • Staff identity. created_by_principal_id, added_by_principal_id, removed_by_principal_id and their siblings stay off. This mirrors the rule already written at session_pose_overrides: the patient receives the fact and the clinical rationale, not the identity of the individual who acted. A specialist's name is that specialist's personal data, not the patient's.
  • Blind indexes and secrets. national_id_hmac stays off — it is a search index, not information about the person, and exporting it leaks the construction of the index.
  • Audit-only forensics. signed_via_ip and signed_user_agent stay audit_only; they exist to prove a signature happened, and the class is the reason they are not part of the subject's copy.
  • Other data subjects. plan_members is untouched: a row names somebody ELSE who is on the plan, and an Art. 15 request is not a route to another person's data. The subscription itself is exported; its roster is not.
  • Commercial internals. patient_subscriptions.payment_provider and external_subscription_id stay off — a Stripe identifier is a fact about a billing integration, not about the patient.

Two NAME columns outside the patient's own tables are granted alongside them: organizations.name and patient_tiers.name. The clinic's name is not optional archive decoration — Art. 15(1)(c) obliges the controller to tell the data subject the recipients of their data, so an archive listing clinics by UUID would be non-compliant on its face. organizations.name is already classified public (clinic naming is a marketing surface), so this widens nothing. patient_tiers.name is the clinic-authored plan label the patient is already shown on their own subscription page.

national_id_encrypted already carried bulk_export before this amendment and keeps it. The archive renders the CNP decrypted, which is the same disclosure the patient's own profile page already makes to them (Art. 15 entitles them to it); the column name reflects storage, not wire format.

patient_caregivers

ColumnClassEgress
patient_profile_idsystem_metadatasupport_export
caregiver_human_idsystem_metadatasupport_export
relationshippii_basicsupport_export
is_legal_representativepii_basicsupport_export
representative_basispii_basicsupport_export
representative_attested_by_principal_idsystem_metadatasupport_export
representative_attested_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export

representative_basis is pii_basic rather than metadata because it is free text a clinic types about a person's legal standing — a court order number, a guardianship reference. It says something about the subject's capacity, which is among the more sensitive things this schema records about anyone, and it must never leave on a marketing or analytics path.

patients

ColumnClassEgress
idsystem_metadatasupport_export, bulk_export
organization_idsystem_metadatasupport_export, bulk_export
patient_profile_idsystem_metadatasupport_export, bulk_export
consumer_idorg_internalsupport_export
patient_numberorg_internalsupport_export, patient_document, bulk_export
last_used_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export, bulk_export
created_atsystem_metadatasupport_export, bulk_export
updated_atsystem_metadatasupport_export, bulk_export

Patient subscriptions

patient_subscriptions

ColumnClassEgress
idsystem_metadatasupport_export, bulk_export
organization_idsystem_metadatasupport_export, bulk_export
patient_idsystem_metadatasupport_export, bulk_export
tier_idsystem_metadatasupport_export, bulk_export
tier_versionorg_internalsupport_export, bulk_export
statusorg_internalsupport_export, bulk_export
started_atsystem_metadatasupport_export, bulk_export
current_period_starts_atorg_internalsupport_export, bulk_export
current_period_ends_atorg_internalsupport_export, bulk_export
cancel_atorg_internalsupport_export, bulk_export
canceled_atorg_internalsupport_export, bulk_export
payment_providerorg_internalsupport_export
external_subscription_idorg_internalsupport_export
created_atsystem_metadatasupport_export, bulk_export
updated_atsystem_metadatasupport_export, bulk_export

patient_subscription_entitlements

ColumnClassEgress
subscription_idsystem_metadatasupport_export
entitlement_codeorg_internalsupport_export
enabledorg_internalsupport_export
created_atsystem_metadatasupport_export

patient_subscription_limits

ColumnClassEgress
subscription_idsystem_metadatasupport_export
limit_codeorg_internalsupport_export
cap_valueorg_internalsupport_export
behaviororg_internalsupport_export
created_atsystem_metadatasupport_export

patient_subscription_overrides

ColumnClassEgress
idsystem_metadatasupport_export
subscription_idsystem_metadatasupport_export
override_kindorg_internalsupport_export
entitlement_codeorg_internalsupport_export
entitlement_enabledorg_internalsupport_export
limit_codeorg_internalsupport_export
cap_valueorg_internalsupport_export
behavior_overrideorg_internalsupport_export
granted_by_principal_idorg_internalsupport_export
reasonorg_internalsupport_export
effective_fromorg_internalsupport_export
expires_atorg_internalsupport_export
revoked_atorg_internalsupport_export
revoked_by_principal_idorg_internalsupport_export
created_atsystem_metadatasupport_export

Plan members

Who else a subscription covers — "people on your plan". Commercial, not clinical: the row says somebody's subscription pays for somebody else and nothing about anyone's care, which is why nothing here rises above org_internal and why being on a plan grants no sight of a member's record.

Membership dates are org_internal rather than system_metadata: left_at is what the 90-day rejoin cooldown is measured from and what answers a seat dispute, so it is business data the clinic reasons about, not an operational timestamp.

plan_members

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
owner_patient_profile_idsystem_metadatasupport_export
member_patient_profile_idsystem_metadatasupport_export
joined_atorg_internalsupport_export
left_atorg_internalsupport_export
added_by_principal_idorg_internalsupport_export
removed_by_principal_idorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Consents

ColumnClassEgress
codepublicbulk_export, support_export
scopepublicbulk_export, support_export
namepublicbulk_export, support_export
descriptionpublicbulk_export, support_export
translationspublicbulk_export, support_export
legal_basispublicbulk_export, support_export
withdrawablepublicbulk_export, support_export
created_atsystem_metadatabulk_export, support_export
enforcementpublicbulk_export, support_export
requires_instrumentpublicbulk_export, support_export
ColumnClassEgress
idsystem_metadatabulk_export, support_export
purpose_codepublicbulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
versionpublicbulk_export, support_export
revisionpublicbulk_export, support_export
body_translationspublicbulk_export, support_export
body_formatpublicbulk_export, support_export
published_atsystem_metadatabulk_export, support_export
published_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatabulk_export, support_export

consents

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
patient_profile_idpii_basicbulk_export, support_export
purpose_codeorg_internalbulk_export, support_export
purpose_versionsystem_metadatabulk_export, support_export
purpose_version_idsystem_metadatabulk_export, support_export
sourceaudit_onlybulk_export, support_export
source_form_idsystem_metadatabulk_export, support_export
granted_ataudit_onlybulk_export, support_export
granted_by_principal_idpii_basicbulk_export, support_export
granted_via_ipaudit_onlysupport_export
withdrawn_ataudit_onlybulk_export, support_export
withdrawn_by_principal_idpii_basicbulk_export, support_export
withdrawal_reasonpii_basicbulk_export, support_export
created_atsystem_metadatabulk_export, support_export

Clinic-owned instruments — the documents a patient reads before agreeing to something. Two families: terms and privacy_notice are assembled from platform templates per 1B.10, while the four clinical consents (telemedicine, video_recording, biometric_capture, telerehab) are authored by the clinic from blank, because drafting a controller's clinical text at the platform would be GDPR Art. 26 joint-controllership drift.

Editor state lives in organization_legal_documents; the immutable artefact patients accept is the corresponding row in consent_purpose_versions (already classified above). authored_body_translations is org_internal for the same reason placeholder_values is — it is the clinic's draft, and the published copy is what a patient ever sees.

Platform catalog. Bodies are public-by-design (the same way consent_purposes is) — they're the scaffolding clinics fill in, not clinic-specific data.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
document_typepublicbulk_export, support_export
versionpublicbulk_export, support_export
localepublicbulk_export, support_export
body_with_placeholderspublicbulk_export, support_export
required_placeholderspublicbulk_export, support_export
toggleable_sectionspublicbulk_export, support_export
published_atsystem_metadatabulk_export, support_export
published_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatabulk_export, support_export

Per-org editor state. placeholder_values carries clinic-identifying fields (clinic name, DPO email, registered address) — not patient-identifying, but the registered DPO email is regulated contact data that belongs in org_internal, not public. The consent_purpose_versions row produced at publish time is what patients see; this table is editor scratch space.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
document_typepublicbulk_export, support_export
source_template_versionpublicbulk_export, support_export
placeholder_valuesorg_internalbulk_export, support_export
included_sectionsorg_internalbulk_export, support_export
authored_body_translationsorg_internalbulk_export, support_export
published_versionsystem_metadatabulk_export, support_export
published_revisionsystem_metadatabulk_export, support_export
published_against_template_versionsystem_metadatabulk_export, support_export
last_reviewed_by_principal_idsystem_metadatasupport_export
last_reviewed_ataudit_onlybulk_export, support_export
created_atsystem_metadatabulk_export, support_export
updated_atsystem_metadatabulk_export, support_export

Notifications

The outbox + per-channel-delivery + dedup-guard + sparse-prefs tables for the platform's notification primitive (Foundation 1A.18). The rendered subject + body live on notifications directly: GDPR access (Art. 15) returns the recipient's row verbatim; support export ships the same shape. Per-delivery transitions on notification_deliveries are operational metadata — the row's columns ARE the forensic record (no audit_log row written by the dispatcher per CLAUDE.md "Operational-metadata bumps are exempt"). notifications + notification_deliveries are range-partitioned monthly in lockstep (P41); notification_idempotency_keys is the flat (non-partitioned) producer-dedup guard.

notifications

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
recipient_principal_idsystem_metadatabulk_export, support_export
recipient_emailpii_basicbulk_export, support_export
categoryorg_internalbulk_export, support_export
idempotency_keysystem_metadatasupport_export
localeorg_internalbulk_export, support_export
timezoneorg_internalbulk_export, support_export
subjectpii_basicbulk_export, support_export
body_textpii_basicbulk_export, support_export
body_htmlpii_basicbulk_export, support_export
patient_profile_idsystem_metadatabulk_export, support_export
attachmentsclinicalsupport_export
scheduled_atsystem_metadatabulk_export, support_export
created_atsystem_metadatabulk_export, support_export

notification_schedules

What a scheduled notification is about, so it can be called off if that thing stops existing. Flat rather than partitioned: the lookup is by entity id and has no time bound, so partitioning it would mean scanning every month. Holds identifiers only — never a recipient, never rendered content.

ColumnClassEgress
notification_idsystem_metadatasupport_export
notification_created_atsystem_metadatasupport_export
source_entity_typeorg_internalsupport_export
source_entity_idsystem_metadatasupport_export
scheduled_atsystem_metadatabulk_export, support_export
created_atsystem_metadatasupport_export

notification_deliveries

ColumnClassEgress
idsystem_metadatabulk_export, support_export
notification_idsystem_metadatabulk_export, support_export
notification_created_atsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
channelorg_internalbulk_export, support_export
statusorg_internalbulk_export, support_export
attemptssystem_metadatasupport_export
claimed_atsystem_metadatasupport_export
claimed_by_worker_idsystem_metadatasupport_export
next_attempt_atsystem_metadatasupport_export
sent_atsystem_metadatabulk_export, support_export
provider_message_idsystem_metadatasupport_export
last_errororg_internalsupport_export
suppressed_reasonorg_internalbulk_export, support_export
delivered_atsystem_metadatabulk_export, support_export
bounced_atsystem_metadatabulk_export, support_export
bounce_typeorg_internalbulk_export, support_export
complained_atsystem_metadatabulk_export, support_export
read_atsystem_metadatabulk_export, support_export
created_atsystem_metadatabulk_export, support_export

notification_idempotency_keys

Producer-side dedup guard (non-partitioned). Holds no PII — an opaque (category, idempotency_key) claim plus the resolved notification id. Ships to support_export only (operational debugging of "why was this not re-sent").

ColumnClassEgress
categoryorg_internalsupport_export
idempotency_keysystem_metadatasupport_export
notification_idsystem_metadatasupport_export
notification_created_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export

notification_suppression

Addresses the platform must not mail, and why. email_address is pii_basic — the row exists precisely to name a person's mailbox — so it ships to support_export only, never to bulk_export: a deliverability list is not part of a clinic's data export, and the platform-scope rows (hard bounces) are other clinics' patients. Release is soft (released_at), because "we stopped mailing this person, then someone decided we could again" is the sequence an authority asks about.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
email_addresspii_basicsupport_export
reasonorg_internalsupport_export
provider_subtypeorg_internalsupport_export
source_delivery_idsystem_metadatasupport_export
notesorg_internalsupport_export
created_atsystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
released_atsystem_metadatasupport_export
released_by_principal_idsystem_metadatasupport_export
release_reasonorg_internalsupport_export

platform_notification_policy

The platform's emergency stop on a notification category — what you reach for when a template renders wrongly or a producer loops, never to express a preference. Sparse: a row exists only where the platform has intervened. reason is NOT NULL because a category stopped without one is a category nobody can safely turn back on. Holds no patient data; it names a category and why it was halted.

ColumnClassEgress
categoryorg_internalsupport_export
enabledorg_internalsupport_export
reasonorg_internalsupport_export
updated_atsystem_metadatasupport_export
updated_by_principal_idsystem_metadatasupport_export

organization_notification_policy

Which notification categories a clinic sends. Sparse — a row exists only where the clinic differs from the category default. Tenant-scope categories only: a clinic cannot switch off the platform-scope notices that exist to warn it about its own account. Holds no patient data; it is a configuration decision about the clinic's own service, so it ships with the rest of the clinic's settings.

ColumnClassEgress
organization_idsystem_metadatabulk_export, support_export
categoryorg_internalbulk_export, support_export
enabledorg_internalbulk_export, support_export
configorg_internalbulk_export, support_export
updated_atsystem_metadatabulk_export, support_export

notification_sending_pauses

A clinic whose bounce or complaint rate reached the point of endangering every other clinic on the shared SES account, and the measurements that said so. The snapshot columns exist because the rates are derived live over a rolling window — by the time anyone reads the row, the window has moved and the numbers that triggered it are gone. Holds no patient data at all: the row is about an organisation's sending behaviour in aggregate, never about any individual recipient. Ships to bulk_export because a clinic is entitled to its own account history.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
paused_atsystem_metadatabulk_export, support_export
bounce_rateorg_internalbulk_export, support_export
complaint_rateorg_internalbulk_export, support_export
sends_in_windoworg_internalbulk_export, support_export
window_dayssystem_metadatabulk_export, support_export
paused_by_principal_idsystem_metadatabulk_export, support_export
lifted_atsystem_metadatabulk_export, support_export
lifted_by_principal_idsystem_metadatabulk_export, support_export
lift_reasonorg_internalbulk_export, support_export

notification_preferences

ColumnClassEgress
recipient_principal_idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
categoryorg_internalbulk_export, support_export
channelorg_internalbulk_export, support_export
enabledorg_internalbulk_export, support_export
updated_atsystem_metadatabulk_export, support_export

Break-glass sessions

Platform-staff elevation records (Foundation 1B.11). Every row is the forensic record of "platform staff X opened time-bound elevated access against clinic Y at scope Z, justified by reason R, between times T0 and T1." Audit_log rows written during the open window carry break_glass_id linking back. Reason fields can carry support-context PII ("looking up patient John Doe per ticket #42") so they ship to support_export only — the audit story for the patient + clinic is the bounded session row + linked audit_log entries, not these reason fields.

break_glass_sessions

ColumnClassEgress
idsystem_metadatabulk_export, support_export
principal_idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
scopeorg_internalbulk_export, support_export
reason_categoryorg_internalbulk_export, support_export
reason_textpii_basicsupport_export
reason_reforg_internalsupport_export
opened_atsystem_metadatabulk_export, support_export
expires_atsystem_metadatabulk_export, support_export
closed_atsystem_metadatabulk_export, support_export
closed_by_principal_idsystem_metadatabulk_export, support_export

Patient impersonation sessions

Clinic-internal access pattern (Foundation 1B.13). Every row records "clinic staff X opened a time-bound session to act on patient Y's behalf at clinic Z, justified by reason R, between T0 and T1." Audit_log rows written during the open window carry impersonation_id linking back. Lives entirely within one clinic's controllership scope (per-clinic counterpart to break-glass; not a controller/processor concern). Reason can carry support context that mentions clinical scenarios ("patient called in confused about their treatment plan") so it ships to support_export only.

patient_impersonation_sessions

ColumnClassEgress
idsystem_metadatabulk_export, support_export
staff_principal_idsystem_metadatabulk_export, support_export
target_patient_idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
reasonpii_basicsupport_export
opened_atsystem_metadatabulk_export, support_export
expires_atsystem_metadatabulk_export, support_export
closed_atsystem_metadatabulk_export, support_export
closed_by_principal_idsystem_metadatabulk_export, support_export

Org-scoped invite primitives (Foundation 1B.12). organization_invites is a per-recipient personal invite (staff or patient) keyed to a Clerk-side invitation; share_links is a code-anchored multi-use redemption primitive (patient-only). Both are state, not events — flat tables with low cardinality per org. Email lives at pii_basic — same posture as humans.email — and never leaves on bulk_export (which is the GDPR-export pipeline scoped to the inviting clinic, not the recipient). The Clerk invitation id is opaque external metadata, support_export-only. patient_profile_id names the EXISTING person a patient invitation hands a login to (the P7 claim) — a foreign key and nothing more, so system_metadata like its specialist_id sibling: it identifies a row, it does not describe a human. What that person's profile CONTAINS is classified on patient_profiles and gated there.

organization_invites

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
provider_invitation_idsystem_metadatasupport_export
emailpii_basicsupport_export
kindorg_internalsupport_export
role_idsystem_metadatasupport_export
patient_tier_idsystem_metadatasupport_export
specialist_idsystem_metadatasupport_export
patient_profile_idsystem_metadatasupport_export
invited_by_principal_idsystem_metadatasupport_export
invited_atsystem_metadatasupport_export
expires_atsystem_metadatasupport_export
accepted_atsystem_metadatasupport_export
accepted_principal_idsystem_metadatasupport_export
consumed_atsystem_metadatasupport_export
revoked_atsystem_metadatasupport_export
revoked_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
codeorg_internalsupport_export
kindorg_internalsupport_export
patient_tier_idsystem_metadatasupport_export
max_usesorg_internalsupport_export
use_countorg_internalsupport_export
expires_atsystem_metadatasupport_export
noteorg_internalsupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
revoked_atsystem_metadatasupport_export
revoked_by_principal_idsystem_metadatasupport_export

Locations

Physical clinic locations (Foundation 1B.14). One row per (org × site); state, not events. Address fields ship pii_basic because a small specialty clinic's location list — combined with appointment data downstream — could enable patient-identity inference; conservative posture. name and slug are public (clinic naming is a marketing surface). timezone, phone, email, and status are operational metadata at org_internal. No bulk_export egress on PII fields — locations are clinic operational data, not patient-export data.

locations

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
slugpublicsupport_export
namepublicsupport_export
timezoneorg_internalsupport_export
phoneorg_internalsupport_export
emailorg_internalsupport_export
address_line1pii_basicsupport_export
address_line2pii_basicsupport_export
citypii_basicsupport_export
countypii_basicsupport_export
postal_codepii_basicsupport_export
countrypii_basicsupport_export
statusorg_internalsupport_export
closed_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Platform service providers

Cat A provider resolution table (Foundation 1C.2). Holds platform-default and per-org-override credentials for capabilities like email, storage, auth, and (future) SMS, video, AI, payments. credentials_encrypted is auth_secret — never leaves the tenant, no egress targets. The non-secret operational columns (provider_name, capability, status, healthcheck metadata) are org_internal with support_export so platform support staff can investigate broken provider rows. config is org_internal; per-provider config payloads must be reviewed when a new provider ships — anything sensitive in config is a bug (move it to credentials_encrypted).

platform_service_providers

ColumnClassEgress
idsystem_metadatasupport_export
capabilityorg_internalsupport_export
organization_idsystem_metadatasupport_export
provider_nameorg_internalsupport_export
credentials_encryptedauth_secret
configorg_internalsupport_export
statusorg_internalsupport_export
last_error_atorg_internalsupport_export
last_errororg_internalsupport_export
last_health_check_atorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Outbound webhook subscriptions

Cat C outbound webhook subscriptions and per-attempt deliveries (Foundation 1C.4). Subscriptions are clinic-managed integrations that POST signed event payloads to clinic-controlled URLs (Make.com, Zapier, n8n, custom backends). signing_secret_encrypted and signing_secret_previous_encrypted are auth_secret — never leave the tenant, no egress targets; the dual-secret rotation window keeps both populated for 24h after a rotation. Operational columns (target_url, event_filters, status, failure_count, success/failure timestamps) are org_internal with support_export so platform support can investigate broken integrations.

outbound_webhook_deliveries is one row per attempt, range-partitioned monthly per P41. The payload column is the full envelope (event, event_id, occurred_at, organization_id, data) snapshotted at enqueue — variable class. By the locked design, the deliveries table inherits the most-permissive class of any included event payload; in practice no event payload sets a class higher than support_export, so the table is support_export only and never feeds bulk_export / analytics_internal / marketing_email. When a future event payload registers a more sensitive class, this table inherits the constraint.

outbound_webhook_subscriptions

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
target_urlorg_internalsupport_export
signing_secret_encryptedauth_secret
signing_secret_previous_encryptedauth_secret
signing_secret_rotated_atsystem_metadatasupport_export
event_filtersorg_internalsupport_export
statusorg_internalsupport_export
failure_countsystem_metadatasupport_export
last_success_atsystem_metadatasupport_export
last_failure_atsystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

outbound_webhook_deliveries

ColumnClassEgress
idsystem_metadatasupport_export
subscription_idsystem_metadatasupport_export
event_idsystem_metadatasupport_export
event_nameorg_internalsupport_export
payloadorg_internalsupport_export
statussystem_metadatasupport_export
attempt_countsystem_metadatasupport_export
next_attempt_atsystem_metadatasupport_export
claimed_atsystem_metadatasupport_export
claimed_by_worker_idsystem_metadatasupport_export
last_attempt_atsystem_metadatasupport_export
last_response_status_codesystem_metadatasupport_export
last_response_bodyorg_internalsupport_export
dead_lettered_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export

Connected Accounts

Cat B Connected Accounts catalog and per-org connections (Foundation 1C.5). The catalog (integration_services) is platform-scoped — clinics consume but never write — and is public because the marketplace landing page renders it pre-auth. Per-org connections (organization_integrations) are org_internal plus the credentials_encrypted column which is auth_secret (no egress; mirrors platform_service_providers.credentials_encrypted and outbound_webhook_subscriptions.signing_secret_encrypted). The config column is variable-class — per-service config payloads must be reviewed when each F-tier connector ships, anything sensitive in config is a bug (move it to credentials_encrypted).

integration_services

ColumnClassEgress
idpublicsupport_export
slugpublicsupport_export
namepublicsupport_export
descriptionpublicsupport_export
auth_typepublicsupport_export
oauth_scopespublicsupport_export
oauth_client_capabilitysystem_metadatasupport_export
icon_urlpublicsupport_export
statuspublicsupport_export
config_schemapublicsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

organization_integrations

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
integration_service_idsystem_metadatasupport_export
auth_typeorg_internalsupport_export
external_account_idorg_internalsupport_export
titleorg_internalsupport_export
statusorg_internalsupport_export
oauth_expires_atsystem_metadatasupport_export
credentials_encryptedauth_secret
configorg_internalsupport_export
last_used_atsystem_metadatasupport_export
last_error_atsystem_metadatasupport_export
last_errororg_internalsupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
inbound_tokensystem_metadatasupport_export
inbound_signing_secret_encryptedauth_secret

Inbound webhook dedup

Operational dedup table for the Cat D Inbound Webhook Convention (Foundation 1C.6). One row per (provider, event_id) we've processed. Range-partitioned monthly per P41; not tenant-scoped — provider events arrive at platform-level /webhooks/{provider} endpoints whose handlers resolve the org from the payload after dedup. AdminPool-only by REVOKE; the table is invisible to restartix_app for both reads and writes. No clinic-facing surface, no egress beyond support_export for incident investigation.

inbound_webhook_dedup

ColumnClassEgress
providersystem_metadatasupport_export
event_idsystem_metadatasupport_export
processed_atsystem_metadatasupport_export

Metering & quotas

Per-capability usage records, live per-org quotas, and closed-period summaries (Foundation 1C.7). Counts and timestamps only — no patient data, no message content. Capability codes are platform-internal taxonomy. AdminPool writes; SELECT gated on the per-org usage.view_org permission so clinic admins can audit their own usage and bill-relevant aggregates. No external egress beyond support_export and the telemetry pipe (organization_id pseudonymized at forwarding) — billing reconstruction stays internal until the billing engine ships.

usage_records.metadata is variable-class: foundation consumers (notify.email at 1C.7) write {}. The first capability that puts identifiable shape into metadata registers the column on a per-capability filter (P39) and lifts the classification accordingly.

usage_records

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
capabilitysystem_metadatasupport_export
unitssystem_metadatasupport_export
unit_typesystem_metadatasupport_export
cost_centssystem_metadatasupport_export
principal_idsystem_metadatasupport_export
occurred_atsystem_metadatasupport_export
metadatasystem_metadatasupport_export

usage_quotas

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
capabilitysystem_metadatasupport_export
periodsystem_metadatasupport_export
limit_unitssystem_metadatasupport_export
current_unitssystem_metadatasupport_export
period_start_atsystem_metadatasupport_export
period_end_atsystem_metadatasupport_export
last_reset_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

usage_summaries

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
capabilitysystem_metadatasupport_export
periodsystem_metadatasupport_export
period_start_atsystem_metadatasupport_export
period_end_atsystem_metadatasupport_export
total_unitssystem_metadatasupport_export
total_cost_centssystem_metadatasupport_export
calls_countsystem_metadatasupport_export
created_atsystem_metadatasupport_export

AI model registry

ai_models is public-by-design — registered models are surfaced on patient-facing AI transparency UIs ("this output was produced by Claude Opus 4.7") so the column class is org_internal with a support_export egress target. ai_model_pricing_history is the inverse: pricing detail is platform-confidential (margin disclosure + commercial contracts), no SELECT policy on the table, AdminPool-only — the columns carry the audit_only class with no support_export egress, so a leaked pricing row never reaches a clinic egress channel even if RLS is misconfigured.

ai_models

ColumnClassEgress
idsystem_metadatasupport_export
model_providerorg_internalsupport_export
model_nameorg_internalsupport_export
model_versionorg_internalsupport_export
capabilityorg_internalsupport_export
unit_typesystem_metadatasupport_export
validation_statusorg_internalsupport_export
validation_notesorg_internalsupport_export
statusorg_internalsupport_export
introduced_atsystem_metadatasupport_export
retired_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

ai_model_pricing_history

ColumnClassEgress
idsystem_metadata
model_idsystem_metadata
cost_per_input_unit_centsaudit_only
cost_per_output_unit_centsaudit_only
effective_fromaudit_only
effective_toaudit_only
changed_by_principal_idaudit_only
notesaudit_only
created_atsystem_metadata

Exercise library

exercises and exercise_renders are platform-curated catalog tables: every authenticated principal SELECTs them (specialists browse the library while authoring treatment plans; patients see published rows in their portal catalog), and mutations route through AdminPool only — the same public-by-design model as ai_models. No PII: every column is either operational metadata (slugs, lifecycle status, hashes, durations, render-queue lease bookkeeping — claimed_at / claimed_by_worker_id / attempts / next_attempt_at) or links to external rendering systems (Bunny video IDs, collection IDs). Most columns carry org_internal or system_metadata with support_export egress so exports for ops debugging can ship rendered-video state alongside the rest of the platform-config payload. The three catalog_thumbnail_* columns are public: they hold Bunny Storage Zone CDN URLs that are served unauthenticated by design — the catalog thumbnail asset is the same URL across Console / Clinic / Portal and is explicitly the platform's public face. reference_code (the shareable EX-NNNN catalog code) is likewise public — it's the patient-quotable, language-neutral identifier shown across every surface; reference_number is its internal backing counter (system_metadata).

exercises

ColumnClassEgress
idsystem_metadatasupport_export
ownership_kindorg_internalsupport_export
organization_idsystem_metadatasupport_export
slugorg_internalsupport_export
reference_numbersystem_metadatasupport_export
reference_codepublicsupport_export
namepublicsupport_export
descriptionpublicsupport_export
translationspublicsupport_export
kindorg_internalsupport_export
statusorg_internalsupport_export
asset_versionsystem_metadatasupport_export
default_preview_render_idsystem_metadatasupport_export
video_collection_idorg_internalsupport_export
catalog_thumbnail_loop_urlpublicsupport_export
catalog_thumbnail_poster_urlpublicsupport_export
manifest_versionsystem_metadatasupport_export
lateralityorg_internalsupport_export
languagesorg_internalsupport_export
capabilitiesorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_renders

ColumnClassEgress
idsystem_metadatasupport_export
exercise_idsystem_metadatasupport_export
recipe_hashsystem_metadatasupport_export
languageorg_internalsupport_export
recipeorg_internalsupport_export
content_file_idsystem_metadatasupport_export
manifest_urlorg_internalsupport_export
manifest_versionsystem_metadatasupport_export
statusorg_internalsupport_export
asset_versionsystem_metadatasupport_export
duration_secondssystem_metadatasupport_export
picksorg_internalsupport_export
rendered_atsystem_metadatasupport_export
failed_reasonorg_internalsupport_export
claimed_atsystem_metadatasupport_export
claimed_by_worker_idsystem_metadatasupport_export
attemptssystem_metadatasupport_export
next_attempt_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export

Exercise taxonomy (F9.1 Phase 2 Sub-phase A)

The F9.1 Phase 2 Sub-phase A expansion adds the full clinical/biomechanical exercise taxonomy on top of the F9.1 Phase 1 exercises row. Design source of truth: exercise-taxonomy-pose-tracking.md; schema reflection: data-model.md Area 9. Every table in this section is platform/clinic-curated catalog content with no patient-identifying fields; rows follow the exercises / exercise_renders pattern — system_metadata or org_internal with support_export egress only. Sub-phase B (pose_engines, pose_landmarks) is registered in the pose-tracking foundation section; Sub-phase C (exercise_pose_configs, exercise_pose_config_history, exercise_pose_landmarks, exercise_pose_metrics, exercise_pose_feedback_rules, pose_data_quality_overrides) is registered in the pose-tracking per-exercise config section.

Class IIa provenance columns (tagged_by_principal_id, tagged_at, clinical_basis) appear on every tag association row per D3. tagged_by_principal_id and tagged_at are system_metadata (the same shape as created_by_principal_id / created_at on other catalog rows); clinical_basis is org_internal (free-text clinical rationale authored by a platform or clinic curator — catalog content, not patient data). All three egress to support_export only; they are not patient PII so they do not flow to bulk_export. Per-tag deprecation columns (deprecated_at, replaced_by_id) appear on every tag entity per D4 and are system_metadata.

exercise_categories

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
slugorg_internalsupport_export
descriptionorg_internalsupport_export
parent_idsystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
deprecated_atsystem_metadatasupport_export
replaced_by_idsystem_metadatasupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_body_regions

Platform-only per D5 (cohort analytics require comparable vocabulary across clinics). organization_id is held NULL by CHECK constraint.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
slugorg_internalsupport_export
body_areaorg_internalsupport_export
sort_ordersystem_metadatasupport_export
deprecated_atsystem_metadatasupport_export
replaced_by_idsystem_metadatasupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_equipment

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
slugorg_internalsupport_export
icon_urlorg_internalsupport_export
sort_ordersystem_metadatasupport_export
deprecated_atsystem_metadatasupport_export
replaced_by_idsystem_metadatasupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_movement_patterns

Platform-only per D5 (pose-engine rep-counting heuristics map to these). organization_id is held NULL by CHECK constraint.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
slugorg_internalsupport_export
descriptionorg_internalsupport_export
sort_ordersystem_metadatasupport_export
deprecated_atsystem_metadatasupport_export
replaced_by_idsystem_metadatasupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_recovery_phases

Platform-only per D5 (comparable across clinics for cohort analytics). organization_id is held NULL by CHECK constraint.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
slugorg_internalsupport_export
descriptionorg_internalsupport_export
sort_ordersystem_metadatasupport_export
deprecated_atsystem_metadatasupport_export
replaced_by_idsystem_metadatasupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_conditions

Platform-canonical condition name with optional ICD-10 mapping per B5. icd10_code is org_internal rather than public — it is per-row catalog metadata, not platform branding, and matches the egress posture of the surrounding catalog columns.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
slugorg_internalsupport_export
descriptionorg_internalsupport_export
icd10_codeorg_internalsupport_export
body_region_idsystem_metadatasupport_export
statusorg_internalsupport_export
sort_ordersystem_metadatasupport_export
deprecated_atsystem_metadatasupport_export
replaced_by_idsystem_metadatasupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_skill_prerequisites

Platform-only per D5 (algorithmic program suggestion requires a locked vocabulary). organization_id is held NULL by CHECK constraint.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
slugorg_internalsupport_export
descriptionorg_internalsupport_export
sort_ordersystem_metadatasupport_export
deprecated_atsystem_metadatasupport_export
replaced_by_idsystem_metadatasupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_tags

Polymorphic junction (P24). tag_id resolves against the appropriate tag-entity table per tag_type.

ColumnClassEgress
exercise_idsystem_metadatasupport_export
tag_typeorg_internalsupport_export
tag_idsystem_metadatasupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export

exercise_prerequisites

Self-M2M between exercises (D2) — "Bird Dog before Side Plank" chains for program-builder ordering.

ColumnClassEgress
exercise_idsystem_metadatasupport_export
prerequisite_exercise_idsystem_metadatasupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export

exercise_instructions

ColumnClassEgress
idsystem_metadatasupport_export
exercise_idsystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
titleorg_internalsupport_export
contentorg_internalsupport_export
image_urlorg_internalsupport_export
instruction_typeorg_internalsupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_contraindications

Per B5: freetext condition_name replaced with condition_id FK to exercise_conditions.

ColumnClassEgress
idsystem_metadatasupport_export
exercise_idsystem_metadatasupport_export
condition_idsystem_metadatasupport_export
descriptionorg_internalsupport_export
severityorg_internalsupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export
translationsorg_internalsupport_export
created_atsystem_metadatasupport_export

Pose-tracking foundation (F9.1 Phase 2 Sub-phase B)

The pose-tracking foundation ships two global reference tables consumed by the Sub-phase C per-exercise pose config (and later, telemetry's pose-frame aggregator). Both rows are vendor-catalog content — they describe which pose engines exist and which landmarks each engine emits, not tenant data. No organization_id, no RLS; writes are AdminPool-only (REVOKE on restartix_app). Classification follows the same system_metadata / org_internal floor used throughout the taxonomy section.

pose_engines

Reference table — pose-engine vendor catalog (D15). Read-only at the API layer; seeded with mediapipe.holistic at F9.1 Phase 2.

ColumnClassEgress
idsystem_metadatasupport_export
codeorg_internalsupport_export
display_nameorg_internalsupport_export
vendororg_internalsupport_export
versionorg_internalsupport_export
landmark_catalog_versionsystem_metadatasupport_export
statusorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

pose_landmarks

Per-engine landmark catalog (~543 rows for MediaPipe holistic = 33 pose + 21 left hand + 21 right hand + 468 face) per D17.

ColumnClassEgress
idsystem_metadatasupport_export
engine_idsystem_metadatasupport_export
codeorg_internalsupport_export
display_nameorg_internalsupport_export
display_name_translationsorg_internalsupport_export
body_part_categoryorg_internalsupport_export
statusorg_internalsupport_export
deprecated_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Pose-tracking per-exercise config (F9.1 Phase 2 Sub-phase C)

Per-exercise pose-tracking configuration on top of the Sub-phase B reference catalog: which engine, which landmark subset, which metrics + thresholds, which feedback rules. Tables follow the catalog pattern — system_metadata for internal FKs / versioning pointers, org_internal for clinician-authored operational thresholds and patient-facing display strings; egress to support_export only because nothing here is patient PII. The exception is pose_data_quality_overrides, which reaches into a specific patient's session and therefore follows the protocol_pauses clinical-state pattern (clinical for the override fact + reason, bulk_export so the patient's GDPR archive reflects the override).

exercise_pose_configs

1:1 with exercises per D8; the active pose-tracking configuration. Operational thresholds (camera_distance_cm_min/_max, min_landmark_confidence, rep_success_rule_params) are org_internal authoring content. engine_id and pinned_asset_version are internal FKs / version pointers — system_metadata.

ColumnClassEgress
idsystem_metadatasupport_export
exercise_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
tracking_enabledorg_internalsupport_export
engine_idsystem_metadatasupport_export
camera_angleorg_internalsupport_export
camera_distance_cm_minorg_internalsupport_export
camera_distance_cm_maxorg_internalsupport_export
lighting_requirementorg_internalsupport_export
in_frame_requirementsorg_internalsupport_export
rep_success_rule_typeorg_internalsupport_export
rep_success_rule_paramsorg_internalsupport_export
pinned_asset_versionsystem_metadatasupport_export
min_landmark_confidenceorg_internalsupport_export
statusorg_internalsupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_pose_config_history

Append-only snapshots per D8 — every edit to exercise_pose_configs (including status transitions and asset-version invalidations) writes a full-row snapshot. Required for Class IIa reproducibility. The snapshot columns mirror exercise_pose_configs 1:1 and carry the same class as their source columns.

ColumnClassEgress
idsystem_metadatasupport_export
exercise_pose_config_idsystem_metadatasupport_export
snapshot_atsystem_metadatasupport_export
snapshot_reasonsystem_metadatasupport_export
exercise_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
tracking_enabledorg_internalsupport_export
engine_idsystem_metadatasupport_export
camera_angleorg_internalsupport_export
camera_distance_cm_minorg_internalsupport_export
camera_distance_cm_maxorg_internalsupport_export
lighting_requirementorg_internalsupport_export
in_frame_requirementsorg_internalsupport_export
rep_success_rule_typeorg_internalsupport_export
rep_success_rule_paramsorg_internalsupport_export
pinned_asset_versionsystem_metadatasupport_export
min_landmark_confidenceorg_internalsupport_export
statusorg_internalsupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export
created_atsystem_metadatasupport_export

exercise_pose_landmarks

M2M between a pose config and the landmark subset it tracks per D17.

ColumnClassEgress
exercise_pose_config_idsystem_metadatasupport_export
landmark_idsystem_metadatasupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export

exercise_pose_metrics

Per-config metric definitions per D18. landmark_refs and derived_from_metric_ids are UUID arrays of FKs — system_metadata, the same shape as a scalar internal FK.

ColumnClassEgress
idsystem_metadatasupport_export
exercise_pose_config_idsystem_metadatasupport_export
metric_typeorg_internalsupport_export
target_minorg_internalsupport_export
target_maxorg_internalsupport_export
toleranceorg_internalsupport_export
weight_pctorg_internalsupport_export
landmark_refssystem_metadatasupport_export
derived_from_metric_idssystem_metadatasupport_export
axisorg_internalsupport_export
labelorg_internalsupport_export
label_translationsorg_internalsupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

exercise_pose_feedback_rules

Single table collapsing form-errors and live-warnings per D10 — semantically identical (condition → patient-facing message), differentiated by severity. patient_message and patient_message_translations are clinician-authored display strings (catalog content, not patient PII), so org_internal is the right floor — they belong to the same category as sessions.name / program_phases.name.

ColumnClassEgress
idsystem_metadatasupport_export
exercise_pose_config_idsystem_metadatasupport_export
severityorg_internalsupport_export
condition_expressionorg_internalsupport_export
condition_formatorg_internalsupport_export
patient_messageorg_internalsupport_export
patient_message_translationsorg_internalsupport_export
tagged_by_principal_idsystem_metadatasupport_export
tagged_atsystem_metadatasupport_export
clinical_basisorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

pose_data_quality_overrides

Per B3 — specialist clinical override of session pose data at session_run or session_exercise_event granularity. Reaches into patient-linked clinical data (a session run), so this table follows the protocol_pauses egress pattern, not the catalog pattern: clinical state and the override timestamp flow to bulk_export so the patient's GDPR archive reflects the override; overridden_by_principal_id stays support_export-only matching protocols.approved_by_principal_id (the patient receives the fact of the override and its clinical rationale, not the identity of the specialist who applied it). override_reason is clinical because it carries clinical context about a specific patient's session.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
scopeclinicalbulk_export, support_export
session_run_idsystem_metadatabulk_export, support_export
session_exercise_event_idsystem_metadatabulk_export, support_export
override_reasonclinicalbulk_export, support_export
overridden_by_principal_idsystem_metadatasupport_export
overridden_atclinicalbulk_export, support_export
created_atsystem_metadatabulk_export, support_export

Sessions

The session model is source-agnostic: sessions is the org-scoped template, session_exercises carries per-exercise dose, session_runs is the per-playthrough state (clinical record — patient-authored), session_pain_events is the partitioned append-only event log of mid-session pain reports, and session_exercise_events is the partitioned append-only event log of per-exercise progress milestones (Design C clinical record — counterpart to telemetry's video-QoS heartbeat). Template tables (sessions, session_exercises) are clinic-curated content — names + materials + dose are operational content with no patient-identifying fields, so org_internal is the right floor. Per-run tables (session_runs, session_pain_events, session_exercise_events) carry patient-linked clinical data: status, completion, pain reports, per-exercise progress, post-session VAS/RPE feedback. clinical class everywhere a patient action is recorded; the patient's own GDPR bulk_export receives those rows (bulk_export egress); support troubleshooting receives the operational metadata via support_export. idempotency_key is an opaque retry token with no egress on either table — it has no value to anyone outside the API write path.

sessions

ColumnClassEgress
idsystem_metadatasupport_export
ownership_kindorg_internalsupport_export
organization_idsystem_metadatasupport_export
patient_idsystem_metadatasupport_export
program_idsystem_metadatasupport_export
phase_idsystem_metadatasupport_export
order_in_phasesystem_metadatasupport_export
kindorg_internalsupport_export
nameorg_internalsupport_export
subtitleorg_internalsupport_export
translationsorg_internalsupport_export
cover_urlpublicsupport_export
objectiveorg_internalsupport_export
estimated_duration_ssystem_metadatasupport_export
exercise_countsystem_metadatasupport_export
content_versionsystem_metadatasupport_export
content_updated_atsystem_metadatasupport_export
source_session_idsystem_metadatasupport_export
source_content_versionsystem_metadatasupport_export
content_severed_atsystem_metadatasupport_export
statusorg_internalsupport_export
idempotency_keysystem_metadata
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

session_exercises

ColumnClassEgress
idsystem_metadatasupport_export
session_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
exercise_idsystem_metadatasupport_export
sequence_ordersystem_metadatasupport_export
modeorg_internalsupport_export
setsorg_internalsupport_export
reps_per_setorg_internalsupport_export
hold_secondsorg_internalsupport_export
sideorg_internalsupport_export
rest_between_sets_sorg_internalsupport_export
rest_after_exercise_sorg_internalsupport_export
languageorg_internalsupport_export
seedsystem_metadatasupport_export
asset_versionsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

deleted_at enables the soft-delete-+-insert pattern for mid-treatment edits (see cadence-and-supervision.md): removing / replacing / re-dosing an exercise soft-deletes the row rather than mutating it, preserving historical session_exercise_events.session_exercise_id resolution.

session_runs

status (in_progress / ended_naturally / ended_explicit / auto_closed) describes how the run terminated; closed_reason (silence_timeout / superseded, CHECK-pinned to auto_closed, NULL otherwise) records WHY the platform closed it — silence-sweep cron vs the same-org close-and-restart supersede; completed (boolean) describes whether the patient walked the whole sequence — orthogonal axes per decisions.md → Why session_runs carries both status and completed. Both are server-derived; exercises_completed is the count of completed-kind exercise events at the terminal write. pose_tracking_choice is the patient's biometric consent for this run (GDPR Art. 9 special category, per-run). safety_acknowledged_at + safety_text_version pin which version of the daily safety reminder the patient saw — IEC 62304 / MDR Class I traceability + product-liability evidence; both clinical because they are direct patient acknowledgments. Engagement telemetry has no column here — it runs as legitimate interest, see decisions.md → Why engagement telemetry is legitimate interest, not consent. country + city are coarse location captured at run-create from Cloudflare edge headers (CF-IPCountry / CF-IPCity) — pii_basic (location is personal data, but low-precision and not clinical), processed under legitimate interest for operational analytics on the clinic's own cohort (the superadmin platform-aggregate dashboard, Stream F). Displayed only as aggregate counts-by-location, never as a per-named-patient map; both ride the patient's bulk_export (their own captured location) and support_export.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
patient_idsystem_metadatabulk_export, support_export
patient_profile_idsystem_metadatasupport_export
session_idsystem_metadatabulk_export, support_export
statusclinicalbulk_export, support_export
closed_reasonclinicalbulk_export, support_export
started_atclinicalbulk_export, support_export
completed_atclinicalbulk_export, support_export
completedclinicalbulk_export, support_export
exercises_completedclinicalbulk_export, support_export
pose_tracking_choiceclinicalbulk_export, support_export
safety_acknowledged_atclinicalbulk_export, support_export
safety_text_versionclinicalbulk_export, support_export
feedback_pain_level_nowclinicalbulk_export, support_export
feedback_perceived_effortclinicalbulk_export, support_export
feedback_notesclinicalbulk_export, support_export
idempotency_keysystem_metadata
created_atsystem_metadatabulk_export, support_export
updated_atsystem_metadatabulk_export, support_export
countrypii_basicbulk_export, support_export
citypii_basicbulk_export, support_export

session_pain_events

ColumnClassEgress
idsystem_metadatabulk_export, support_export
run_idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
exercise_idsystem_metadatabulk_export, support_export
set_idxclinicalbulk_export, support_export
sideclinicalbulk_export, support_export
seconds_into_setclinicalbulk_export, support_export
severityclinicalbulk_export, support_export
actionclinicalbulk_export, support_export
reported_region_idsystem_metadatabulk_export, support_export
reported_region_noteclinicalbulk_export, support_export
reported_atclinicalbulk_export, support_export
client_event_idsystem_metadata

session_exercise_events

Design C clinical record: per-exercise milestones (started / completed / skipped / abandoned / paused_for_pain / paused / resumed) the player POSTs as the patient progresses through a run, plus the server-synthesized abandoned event. Same class shape as session_pain_events — every patient action is clinical. Drop point is inferred server-side; the client never fires a dropped kind. abandoned is server-synthesized on non-natural run termination (EndEarly, AutoCloseRun) for any session_exercise that has events but no terminal event; reason discriminates the cause (session_ended for explicit end-session, auto_closed for the silence-sweep). set_count_completed and video_time_s are populated only when meaningful (completed, paused_for_pain) and stay clinical. reason discriminates non-pain pauses (manual / visibility / network), the abandonment cause (session_ended / auto_closed), the skip cause (pain / too_difficult / no_equipment / other), and is mirrored on the matching resumed row; it's clinical because the cause of a pause, skip, or abandonment is part of the clinical narrative ("session took 5m for a 90s video because the network kept dropping" / "patient ended session during foot-slide after reporting pain"). skip_note is the optional patient-entered free-text feedback accompanying any skip (≤200 chars, any reason) — patient-authored clinical narrative, same clinical class.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
run_idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
session_exercise_idsystem_metadatabulk_export, support_export
kindclinicalbulk_export, support_export
reasonclinicalbulk_export, support_export
skip_noteclinicalbulk_export, support_export
video_time_sclinicalbulk_export, support_export
set_count_completedclinicalbulk_export, support_export
occurred_atclinicalbulk_export, support_export
created_atsystem_metadatabulk_export, support_export
client_event_idsystem_metadata

protocols

Renamed from patient_assignments by the 2026-05-23 protocols rename (the "assignment" naming was a relic of the shared-by-reference model where patients were "assigned to" shared programs; under three-tier copy-on-derive the row is the workflow wrapper around a patient-instance program — "protocol" matches the concept). program_id references the PATIENT-INSTANCE program the prescribe/enroll service deep-copied (1:1 with this protocol, never shared), and source_program_id records which platform/org template the instance was copied from (analytics like "patients on the Knee Rehab template family"). Covers both prescriptions (specialist-driven, cadence + adherence) and enrollments (self-initiated, course progress, no adherence). Each row represents a patient's active or historical engagement with a program — clinical context: which program was prescribed, with what cadence, with what supervision mode, over what date range, with what approval state. The cadence_config JSONB is purely operational shape (e.g., {"sessions_per_week": 3} / {"days_of_week": ["tue","thu"]}) carrying no patient-identifiable content, so org_internal. Approval + status + dates + supervision + cadence are first-class clinical state — clinical class. Patient GDPR bulk_export receives the whole row so "what was prescribed to me, when, and by whom" is part of the portability surface; support exports likewise. See cadence-and-supervision.md for column-shape definitions (2026-05-28 renames: modality → supervision_mode; appointment_driven cadence_kind dropped).

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
patient_idsystem_metadatabulk_export, support_export
program_idsystem_metadatabulk_export, support_export
source_program_idsystem_metadatabulk_export, support_export
kindclinicalbulk_export, support_export
supervision_modeclinicalbulk_export, support_export
cadence_kindclinicalbulk_export, support_export
cadence_configorg_internalbulk_export, support_export
statusclinicalbulk_export, support_export
start_dateclinicalbulk_export, support_export
end_dateclinicalbulk_export, support_export
end_date_is_hard_capclinicalbulk_export, support_export
completed_atclinicalbulk_export, support_export
approval_statusclinicalbulk_export, support_export
approved_by_principal_idsystem_metadatasupport_export
required_entitlementorg_internalbulk_export, support_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatabulk_export, support_export
updated_atsystem_metadatabulk_export, support_export

protocol_pauses

Append-only history of pause intervals on a protocol (F9.2 PR 4, renamed 2026-05-23). One row per pause; resumed_at NULL means currently paused. Each pause is patient-visible clinical state (the cadence engine subtracts paused intervals from the adherence denominator), so timestamps + reason are clinical. Patient GDPR bulk_export receives the rows for "when was my protocol paused, by whom, and why."

kind (000051) is clinical for the same reason reason is: it is part of the answer to "why was my treatment stopped for those eleven days." A content_edit pause tells the patient their clinic was retuning the program rather than that a clinician judged they should rest — a materially different fact about their own care, and one they are entitled to on export.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
protocol_idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
paused_atclinicalbulk_export, support_export
resumed_atclinicalbulk_export, support_export
reasonclinicalbulk_export, support_export
kindclinicalbulk_export, support_export
paused_by_principal_idsystem_metadatasupport_export
resumed_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatabulk_export, support_export

TV companion mode

Phase 1 of the phone–TV companion build (000024). session_pairings is the short-lived bridge between an anonymous TV (POST /v1/session-pairings) and an authenticated phone (POST /{pair_id}/claim); the claim atomically creates the session_runs row via sessions.Service.CreateRun. session_tv_liveness is the TV-side heartbeat (10s cadence) the auto-close cron reads to decide whether to mark abandoned runs auto_closed (status) — the cron's partial/unknown classification lives as observability metrics, not as separate status values; the row carries exercises_completed > 0 as the partial-vs-unknown distinguisher. Neither table participates in patient GDPR bulk_export — pairings are ephemeral session-setup mechanics with no patient-readable meaning, and liveness is TV-side operational metadata that exists to prove "the session was running" rather than "what the patient did." Both flow to support_export so platform support can investigate "the pairing code didn't work" / "did the TV ever connect?" tickets. code is auth_secret — within its 5-min TTL the code carries session-level bearer authority; never logged, never exported. display_token_jti is auth_secret (JWT id for revocation; the token itself is never persisted, only its jti is kept for the lifetime of the run for the post-terminal /feedback revocation lookup).

session_pairings

ColumnClassEgress
idsystem_metadatasupport_export
codeauth_secret
organization_idsystem_metadatasupport_export
run_idsystem_metadatasupport_export
claimed_atsystem_metadatasupport_export
claimed_by_principal_idsystem_metadatasupport_export
claim_idempotency_keysystem_metadata
display_token_jtiauth_secret
source_ipaudit_onlysupport_export
expires_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

session_tv_liveness

ColumnClassEgress
idsystem_metadatasupport_export
run_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
tssystem_metadatasupport_export
conductor_statusaudit_onlysupport_export
current_exercise_idxsystem_metadatasupport_export
exercise_video_time_ssystem_metadatasupport_export
network_stateaudit_onlysupport_export
idempotency_keysystem_metadata

Programs & content (F9.2 Phase 1 substrate)

The F9.2 substrate adds the cross-cutting catalog model: content_files registers consumable media (audio / video / image / document); programs containers package sessions into multi-week journeys with optional phases; program_phases / program_assets / session_audio_items / session_assets are owned-child tables that hang off programs or sessions. Sessions point at their parent program directly via sessions.program_id under the three-tier copy-on-derive model (2026-05-22) — the previous program_sessions junction was retired, and per-prescription isolation is provided by server-side deep copies instead of version snapshots (the dropped program_versions / session_versions tables). programs.derived_from_program_id is the flat lineage pointer for org-tier saved variants. Every table is clinic-curated content — names, descriptions, dose configs, file metadata — with no patient-identifying fields except the patient_id on patient-specific tier rows (an FK identifier; the clinical content lives in the referenced session/program columns). Everything is org_internal or system_metadata with support_export egress, matching the exercises and sessions pattern — except the per-phase dosing columns added with the treatment-as-journey spine (2026-08-24), which mirror their protocols namesakes: program_phases.cadence_kind / supervision_mode are clinical, while cadence_config carries the same operational JSONB shape and so stays org_internal.

content_files

ColumnClassEgress
idsystem_metadatasupport_export
ownership_kindorg_internalsupport_export
organization_idsystem_metadatasupport_export
kindorg_internalsupport_export
storage_providerorg_internalsupport_export
storage_reforg_internalsupport_export
mime_typeorg_internalsupport_export
file_size_bytessystem_metadatasupport_export
duration_secondssystem_metadatasupport_export
metadataorg_internalsupport_export
uploaded_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

programs

ColumnClassEgress
idsystem_metadatasupport_export
ownership_kindorg_internalsupport_export
organization_idsystem_metadatasupport_export
patient_idsystem_metadatasupport_export
slugorg_internalsupport_export
nameorg_internalsupport_export
subtitleorg_internalsupport_export
description_htmlorg_internalsupport_export
translationsorg_internalsupport_export
tagsorg_internalsupport_export
cover_urlpublicsupport_export
statusorg_internalsupport_export
derived_from_program_idsystem_metadatasupport_export
structure_versionsystem_metadatasupport_export
structure_updated_atsystem_metadatasupport_export
source_structure_versionsystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

program_phases

ColumnClassEgress
idsystem_metadatasupport_export
program_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
descriptionorg_internalsupport_export
translationsorg_internalsupport_export
order_in_programsystem_metadatasupport_export
entry_criteriaorg_internalsupport_export
requires_unlocksystem_metadatasupport_export
unlocked_atsystem_metadatasupport_export
unlocked_by_principal_idsystem_metadatasupport_export
kindsystem_metadatasupport_export
cadence_kindclinicalsupport_export
cadence_configorg_internalsupport_export
supervision_modeclinicalsupport_export
entered_atsystem_metadatasupport_export
source_phase_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

program_assets

ColumnClassEgress
idsystem_metadatasupport_export
program_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
content_file_idsystem_metadatasupport_export
labelorg_internalsupport_export
order_in_programsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

session_audio_items

ColumnClassEgress
idsystem_metadatasupport_export
session_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
content_file_idsystem_metadatasupport_export
order_in_sessionsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

session_assets

ColumnClassEgress
idsystem_metadatasupport_export
session_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
content_file_idsystem_metadatasupport_export
labelorg_internalsupport_export
order_in_sessionsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Patient catalog (F9.x Phase 1)

The merchandising layer that decouples how content is presented to patients from what the content is. Per-org (organization_id set) plus platform-default (organization_id IS NULL) curation; catalog_entries place programs/sessions into ordered, featured catalog_sections. All clinic-curated presentation config — section/entry names, badges, ordering — no patient-identifying fields. org_internal / system_metadata with support_export, matching the Programs & content family. required_entitlement is reserved for Phase 2 tier-gating.

catalog_sections

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
codeorg_internalsupport_export
nameorg_internalsupport_export
descriptionorg_internalsupport_export
translationsorg_internalsupport_export
sort_ordersystem_metadatasupport_export
statusorg_internalsupport_export
grid_columnssystem_metadatasupport_export
card_aspectsystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

grid_columns and card_aspect are presentation config, classed system_metadata beside sort_order for the same reason: they describe how a shelf is drawn, not what a clinic offers or what a patient did. They reach the patient's browser on every catalog read, which is the point of them.

catalog_entries

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
section_idsystem_metadatasupport_export
content_typeorg_internalsupport_export
content_idsystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
featuredorg_internalsupport_export
required_entitlementorg_internalsupport_export
cover_content_file_idsystem_metadatasupport_export
badgeorg_internalsupport_export
subtitleorg_internalsupport_export
translationsorg_internalsupport_export
statusorg_internalsupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

patient_content_grants

Per-patient ownership of specific catalog content (a program or standalone session), independent of the patient's tier — the third OR-branch of the catalog access check (free OR grant OR tierHas(code)) and the migration target for the ~20k legacy D2C patients' per-program ownership (catalog Phase 3.1). Same access-state family as patient_subscriptions: no patient-identifying content, org_internal / system_metadata, support_export only. reason is curator-entered free text (comp/promo rationale) — org_internal, not clinical.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
patient_idsystem_metadatasupport_export
patient_profile_idsystem_metadatasupport_export
content_typeorg_internalsupport_export
content_idsystem_metadatasupport_export
granted_fromorg_internalsupport_export
granted_untilorg_internalsupport_export
sourceorg_internalsupport_export
granted_by_principal_idsystem_metadatasupport_export
reasonorg_internalsupport_export
fulfillment_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

access_offers

The merchandised access bundle a clinic authors (commerce/campaign provisioning core, 000035). No patient data — an internal label + status + authoring provenance. org_internal / system_metadata, support_export only.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
statusorg_internalsupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

access_offer_items

The grant set of an offer (000035): what content/tier each item grants and on what terms. Typed-polymorphic content_id (no FK) like patient_content_grants. No patient data; org_internal / system_metadata, support_export only.

ColumnClassEgress
idsystem_metadatasupport_export
offer_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
grant_kindorg_internalsupport_export
content_typeorg_internalsupport_export
content_idsystem_metadatasupport_export
tier_idsystem_metadatasupport_export
term_kindorg_internalsupport_export
duration_daysorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

access_offer_fulfillments

Idempotency + forensic ledger of "offer X provisioned to patient Y via trigger Z" (000035). Same access-state family as patient_content_grants: no patient-identifying content beyond the patient_id FK, org_internal / system_metadata, support_export only. trigger_ref is an internal/external order or claim reference, not PII.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
patient_idsystem_metadatasupport_export
offer_idsystem_metadatasupport_export
trigger_typeorg_internalsupport_export
trigger_reforg_internalsupport_export
campaign_item_idsystem_metadatasupport_export
sourceorg_internalsupport_export
provisioned_atsystem_metadatasupport_export
revoked_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export

access_offer_campaigns

The marketing-campaign front door for access offers (000038): a clinic-authored presentation card (headline / blurb / image) bound to an offer, published to the public portal campaign page. The presentation columns are class public (they appear in unauthenticated responses by design); offer_id is the confidential link and stays system_metadata — never egressed, resolved server-side at claim time (access-offers locked decision 3).

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
offer_idsystem_metadatasupport_export
headlinepublicsupport_export
blurbpublicsupport_export
image_urlpublicsupport_export
publishedorg_internalsupport_export
published_atsystem_metadatasupport_export
sort_orderorg_internalsupport_export
featuredorg_internalsupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

access_offer_sku_bindings

Maps a shop product/variation SKU to an access offer (000037). Clinic commerce config — no patient data. org_internal / system_metadata, support_export only.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
integration_idsystem_metadatasupport_export
external_product_reforg_internalsupport_export
external_variation_reforg_internalsupport_export
offer_idsystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

access_offer_orders

Staged paid-order record awaiting the buyer's claim (000037). No PII (order_ref / order_key are order identifiers, not patient data; billing is never persisted here). org_internal / system_metadata, support_export only.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
integration_idsystem_metadatasupport_export
providerorg_internalsupport_export
order_reforg_internalsupport_export
order_keyorg_internalsupport_export
statusorg_internalsupport_export
matched_offer_idssystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Specialists & specialties (F1)

The clinic's provider roster (F1). All three tables are state, org-scoped.

name, title and description are public: a specialist's professional identity is the clinic's marketing surface — it renders on the public booking page (F5.4) to an unauthenticated visitor choosing who to book with. That is a deliberate difference from humans.email, which stays pii_basic; the roster is published, the person's contact details are not.

signature_url and avatar_url have matching names and opposite postures, which is deliberate rather than an oversight.

signature_url holds a private S3 key, not a URL, and has no egress target. A signature is the instrument that makes a prescription legally binding (F6), so it must never leave the tenant through any bulk or support path — a leaked signature image is forgeable. It reaches a browser only as a presigned URL that expires, and reaches a document only as embedded base64.

avatar_url holds a public Bunny CDN URL and is classed public, alongside the name/title/description it appears next to. A clinician's photo is part of the professional identity the clinic publishes: it renders on the public booking page (F5.4) to an unauthenticated visitor, so treating it as confidential would have been a label the system did not honour. Presigning it bought nothing — the same audience sees it either way — while costing a round-trip per row, which is why photos could not appear on the roster at all under the old design.

Two consequences worth stating plainly. The URL is unguessable but permanent and unauthenticated: content-addressing (sha12) means nobody enumerates it, but anyone who has it keeps it, so clearing the column is not enough — the upload path orphan-deletes the CDN object, and that deletion is what actually revokes access. And a photo is still personal data: public describes where it may be served, not that it escapes erasure. A GDPR erasure covering a specialist must delete the object, not merely null the column.

metadata is org_internal and non-clinical by contract — sparse descriptive extras only. A regulated identifier may never be written here; pii_regulated is encrypted BYTEA on patient_profiles and never in a generic value store. Promote a key to a typed column the moment a UI surface needs to filter on it.

scheduling_timezone and scheduling_active are operational scheduling config, not personal data.

specialties

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
titlepublicsupport_export
slugpublicsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

specialist_titles

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
keysystem_metadatasupport_export
titlepublicsupport_export
descriptionorg_internalsupport_export
sort_ordersystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

specialists

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
human_idsystem_metadatasupport_export
namepublicsupport_export, patient_document
titlepublicsupport_export
specialist_title_idsystem_metadatasupport_export
description_htmlpublicsupport_export
slugpublicsupport_export
signature_urlpii_basic
avatar_urlpublicsupport_export
scheduling_timezoneorg_internalsupport_export
scheduling_activeorg_internalsupport_export
metadataorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

specialist_specialties

ColumnClassEgress
specialist_idsystem_metadatasupport_export
specialty_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export

Offerings (F2.1)

The clinic's catalog of clinical services (F2.1) — the configuration spine calendars (F4), appointments (F5) and offering_forms (F3.4) reference. Both tables are state, org-scoped.

title, slug, description and video_url are public for the same reason F1's specialists.name is: the catalog is the clinic's marketing surface and renders on the public booking page (F5.4) to an unauthenticated visitor choosing what to book. video_url is an external URL the clinic itself publishes (its own YouTube/Vimeo presentation clip), not a platform asset — publishing it is the whole point.

cover_url holds a public Bunny CDN URL and is classed public. It used to hold a private S3 key on the reasoning that the rendered image is public but the storage address is not — a distinction that bought nothing here, because a cover depicts nobody, carries no clinical meaning, and exists to be seen by unauthenticated visitors browsing a booking page. What the split did cost was a presign round-trip per catalog card and the ability to server-render the image at all, since a signed URL embedded in a cached page outlives its own signature.

programs.cover_url is the same field for clinical programs and takes the same class for the same reasons — artwork of nobody, on a browse surface, presigned for no one's benefit. It sits on the PROGRAM rather than on catalog_entries because most program cards have no catalog entry: a prescribed program was never catalogued, and an enrolled one is a patient-instance deep copy that is not catalogued either. DeepCopyProgram carries the column, so a patient's instance inherits the template's artwork and the same public class travels with it.

sessions.cover_url is the third instance and takes the same class for the third time — artwork of nobody, on a browse surface. It exists because a STANDALONE session is catalogued in its own right (catalog_entries.content_type = 'session') and had no artwork to show there. Both session copy edges in programs/copy.go list the column, so an attached or prescribed copy inherits the template's artwork along with its public class.

The contrast worth holding onto is specialists.signature_url, which stayed private for exactly the reason this one did not: a leaked signature is forgeable. Public is a property of the content, not a convenience.

Same two consequences as any public asset: the URL is unguessable (content-addressed) but permanent and unauthenticated, so revocation is the CDN object delete the upload path performs, not nulling the column; and a clinic that removes a service should not assume the artwork vanished because the row did.

published, published_at and is_public are org_internal: they describe the clinic's own catalog state, and a draft offering's existence is not something an unauthenticated visitor is entitled to know.

metadata is org_internal and non-clinical by contract, matching specialists.metadata — sparse descriptive extras only, never a regulated identifier.

priority on the junction is the assignment engine's walk order (F4.3), org-internal scheduling config rather than anything about the person.

offerings

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
titlepublicsupport_export, patient_document
slugpublicsupport_export
description_htmlpublicsupport_export
specialty_idsystem_metadatasupport_export
default_duration_minutesorg_internalsupport_export
cover_urlpublicsupport_export
video_urlpublicsupport_export
publishedorg_internalsupport_export
published_atorg_internalsupport_export
is_publicorg_internalsupport_export
metadataorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

offering_specialists

ColumnClassEgress
offering_idsystem_metadatasupport_export
specialist_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
priorityorg_internalsupport_export
created_atsystem_metadatasupport_export

Custom fields (F3.1)

The clinic's reusable field library and the canonical org-scoped value store (F3.1). Both tables are state, org-scoped.

custom_fields is definitions, not datadescription and options are org_internal because they describe how a clinic configures its own intake, and a competitor learning that a clinic asks about smoking history is disclosure the clinic did not choose. They are not public: unlike a specialist's professional identity or an offering's title, a field definition is never a marketing surface. key and system_key are system_metadata — stable identifiers PDF templates, exports and cross-org template copy resolve against.

label additionally carries patient_document, because a printed row needs a caption: the F6 renderer draws the clinic's own label beside the value ("Greutate (kg): 74"). The rest of the definition — description, options, is_private — stays inside the tenant. A document prints what a field holds, never how it was configured.

custom_field_values.value is clinical, and its only egress target is patient_document. It holds whatever the clinic chose to ask a patient — medication, prior surgeries, activity level — so no bulk, support, marketing or AI path has a reason to receive it. The document target is the exception because the recipient is the same clinic that wrote the value, printing it on their own patient's record (A2, 2026-08-07).

No cross-clinic scoping applies to this store, and that is not an omission. patient_profiles is portable across every clinic a patient attends, so a staff read of it has to be scoped to a patients row at the reading org. custom_field_values carries organization_id and never crosses a clinic boundary in the first place — a clinic printing these values is printing what it recorded itself.

A private field is never printed. custom_fields.is_private means staff-only, and a generated document is handed to a patient — so the resolver drops private fields regardless of document type, and the template builder never offers one. That rule is uniform across the platform: it retired the earlier carve-out (D3) under which a medical prescription printed private form answers for transparency.

A pii_regulated value can never appear here, and that is enforced by the custom_field_values_reject_regulated trigger rather than by convention — a field_type = 'national_id' field raises on INSERT/UPDATE. CNP lives once, encrypted, on patient_profiles.

entity_type / entity_id are the polymorphic pair (P24); entity_id is patients.id for entity_type = 'patient', never patient_profiles.id, because this store never crosses a clinic boundary.

custom_fields

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
entity_typesystem_metadatasupport_export
keysystem_metadatasupport_export
labelorg_internalsupport_export, patient_document
field_typesystem_metadatasupport_export
optionsorg_internalsupport_export
descriptionorg_internalsupport_export
is_privateorg_internalsupport_export
sort_ordersystem_metadatasupport_export
system_keysystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

custom_field_values

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
custom_field_idsystem_metadatasupport_export
entity_typesystem_metadatasupport_export
entity_idsystem_metadatasupport_export
valueclinicalpatient_document
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Forms (F3.2 / F3.3 / F3.4)

Form templates, instances, version history and the offering junction (F3). All are org-scoped; forms and form_templates soft-delete.

form_templates.name / description and form_templates.fields are org_internal — a template is the clinic's own authoring work, and the questions it asks reveal how that clinic practises. fields additionally carries the binding map: which shared value each question reads, and whether an answer writes back. That is configuration about patient data rather than patient data itself, which is why it classifies org_internal and not clinical.

forms.values and forms.files are clinical_sensitive with no egress target. This is the free-text clinical record: what a patient answered about their medication, their pain, their history — plus, once signed, the evidence behind a consent. No bulk or support path receives it. A patient reaches their own through the DSAR route, and a clinician through the authenticated UI; neither is a classification egress target.

forms.title / description are org_internal: they come from the template, not from the patient.

fields on an instance is the snapshot — a copy of the template's arrangement, taken at first write. Same class as the template's own fields for the same reason.

form_template_versions.fields_snapshot is org_internal and append-only. It is what a rollback restores from and what answers "what exactly did v1 say" when a patient disputes a consent, so it must survive even when the live template has moved on.

offering_forms is pure configuration — which template attaches in which category of which service.

document_categories is the clinic's paperwork taxonomy and is entirely org_internal configuration. Nothing in it describes a patient: it says which kinds of document this clinic issues, what it calls them, who fills each one, and how many an offering may attach. title and description are the clinic's own words and reveal how that clinic practises — the same reasoning that makes form_templates.name org_internal rather than public — while key, filled_by, cardinality and generatable_on_appointment are structural. key additionally carries webhook_egress: it is the category_key a form.created payload already contains, and a subscriber that receives the key with no way to resolve what it means is receiving noise.

webhook_egress on the forms tables (F3, added 2026-08-04). The Cat E events form.created / .completed / .signed and form_template.published are the first real producers for the Cat C outbound-webhook framework, and a webhook body is delivered verbatim to a URL the clinic configured — so a payload field is an egress and needs a target here. The columns carrying webhook_egress are exactly the ones those four payloads contain: identifiers, the form type and status, the template name and version, and the consent declaration.

What deliberately does NOT carry it is the whole point. forms.values and forms.files are clinical_sensitive with no egress target at all — what a patient wrote about their medication never leaves through an integration. Nor does the signature evidence: signed_name is pii_basic and signed_via_ip / signed_user_agent are audit_only, and none of the three is in a payload. A subscriber that needs more calls the API with its own credentials and gets what its permissions allow, which is the right place for that decision.

Framework gap, not an F3 one. Nothing in the webhook delivery path calls classification.AllowedFor — the registry describes what MAY egress, but the Cat C dispatcher does not consult it, and the event producers that shipped before F3 (organization, designations, portalonboarding, exercises, ownershiptransfer, pose_configs) carry payload fields whose columns have no webhook_egress target. Until that check exists, these entries are a declaration of intent rather than an enforced boundary. Closing it belongs with F11 compliance hardening.

consent_purposes and signature_mode (F3.5, on the template, its version history and the instance snapshot) are org_internal: they say which ledger entries signing this document produces and how the signature is captured. That is authoring configuration, and the consent it eventually produces is a consents row, classified there.

forms.required_signature_mode is what a form DEMANDS (configuration, frozen at first write) and forms.signature_mode is what actually happened (evidence, null until signed). Both are org_internal.

The instance's signature evidence splits. forms.signed_name is the name the patient typed and is pii_basic — it is a name, and it classifies like every other name on the platform. forms.signed_via_ip and forms.signed_user_agent are audit_only: they exist to make a signature defensible if it is ever disputed, not to describe the patient. They carry support_export to match consents.granted_via_ip, which is the same fact recorded from the ledger's side of the same act — a support export that showed the IP behind the consent row but not the IP behind the form it came from would be describing one event twice and disagreeing with itself.

form_templates

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export, webhook_egress
descriptionorg_internalsupport_export
category_idsystem_metadatasupport_export, webhook_egress
fieldsorg_internalsupport_export
versionsystem_metadatasupport_export, webhook_egress
publishedorg_internalsupport_export
published_atorg_internalsupport_export
pdf_template_idsystem_metadatasupport_export
consent_purposesorg_internalsupport_export
signature_modeorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

form_template_versions

ColumnClassEgress
idsystem_metadatasupport_export
form_template_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
versionsystem_metadatasupport_export
fields_snapshotorg_internalsupport_export
consent_purposesorg_internalsupport_export
signature_modeorg_internalsupport_export
published_atsystem_metadatasupport_export
changed_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export

forms

ColumnClassEgress
idsystem_metadatasupport_export, webhook_egress, bulk_export
organization_idsystem_metadatasupport_export, bulk_export
appointment_idsystem_metadatasupport_export, webhook_egress, bulk_export
form_template_idsystem_metadatasupport_export, webhook_egress, bulk_export
template_versionsystem_metadatasupport_export, webhook_egress, bulk_export
fieldsorg_internalsupport_export, bulk_export
patient_profile_idsystem_metadatasupport_export, webhook_egress, bulk_export
titleorg_internalsupport_export, bulk_export
descriptionorg_internalsupport_export, bulk_export
category_keysystem_metadatasupport_export, webhook_egress, bulk_export
valuesclinical_sensitivebulk_export
filesclinical_sensitivebulk_export
sort_ordersystem_metadatasupport_export, bulk_export
statusorg_internalsupport_export, webhook_egress, bulk_export
completed_atorg_internalsupport_export, bulk_export
signed_atorg_internalsupport_export, bulk_export
consent_purposesorg_internalsupport_export, webhook_egress, bulk_export
required_signature_modeorg_internalsupport_export, webhook_egress, bulk_export
signature_modeorg_internalsupport_export, bulk_export
signed_namepii_basicsupport_export, bulk_export
signed_via_ipaudit_onlysupport_export
signed_user_agentaudit_onlysupport_export
created_by_principal_idsystem_metadatasupport_export
signed_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export
trigger_eventsystem_metadatasupport_export

document_categories

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
keysystem_metadatasupport_export, webhook_egress
titleorg_internalsupport_export, webhook_egress
descriptionorg_internalsupport_export
filled_bysystem_metadatasupport_export
cardinalitysystem_metadatasupport_export
generatable_on_appointmentsystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
system_keysystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

offering_forms

ColumnClassEgress
offering_idsystem_metadatasupport_export
form_template_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
category_keysystem_metadatasupport_export
category_is_singlesystem_metadatasupport_export
specialist_title_idsystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
created_atsystem_metadatasupport_export

form_sessions

Token-authorised access to ONE form, for a patient who is not logged in. The raw token is never stored — only its SHA-256, which is auth_secret because it is exactly that: a credential that authorises signing a consent.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
form_idsystem_metadatasupport_export
token_hashauth_secret
modesystem_metadatasupport_export
expires_atsystem_metadatasupport_export
consumed_atsystem_metadatasupport_export
revoked_atsystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

form_triggers

Clinic configuration — which paperwork fires at an event with no appointment behind it. Carries no patient reference of any kind; the forms it produces do.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
form_template_idsystem_metadatasupport_export
trigger_eventsystem_metadatasupport_export
category_keysystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Scheduling (F4)

Booking configuration and specialist availability. Six tables, all state, all org-scoped.

Availability rows are org_internal, not public. specialist_weekly_hours and specialist_schedule_overrides say when a named clinician at a named clinic is physically working, and — via location_id — where. That is the clinic's operational picture, not a marketing surface, and it is the reason both tables carry RLS despite being reachable only through a specialist. Patients never read these rows: the portal reaches availability through the derived-slot endpoint, which is a computed projection that discloses free times without disclosing the schedule that produced them.

calendars splits along the same line offerings does. name, slug and description_html are public — they are what a patient reads on a booking page, exactly like an offering's title. description_html holds SANITISED HTML and nothing else may write it: clinic-authored markup rendered to unauthenticated visitors is a stored-XSS surface, and scheduling.SanitizeDescription with its narrow allowlist is the only thing between a pasted <script> and every visitor to that clinic's booking page. Every booking RULE (slot_duration_minutes, slot_gap_minutes, cooldown_minutes, min_lead_time_minutes, horizon_days, assignment_strategy) is org_internal: a competitor learning that a clinic runs 20-minute slots with a 48-hour cooldown is disclosure the clinic did not choose. The patient-facing consequences of those rules reach the portal as computed slots and structured errors, never as the settings themselves.

specialist_assignment_tracking is org_internal throughout. How a clinic distributes work between its clinicians is an internal management fact.

No table here has a webhook_egress target. F4 publishes no Cat E events yet; when it does, the payload fields need targets added in the same PR, because a webhook body is delivered verbatim to a clinic-configured URL (the rule F3 established).

specialist_weekly_hours

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
specialist_idsystem_metadatasupport_export
day_of_weekorg_internalsupport_export
start_timeorg_internalsupport_export
end_timeorg_internalsupport_export
location_idorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

specialist_schedule_overrides

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
specialist_idsystem_metadatasupport_export
calendar_idsystem_metadatasupport_export
start_dateorg_internalsupport_export
end_dateorg_internalsupport_export
availabilityorg_internalsupport_export
location_idorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

calendars

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
offering_idsystem_metadatasupport_export
namepublicsupport_export
slugpublicsupport_export
description_htmlpublicsupport_export
location_idorg_internalsupport_export
slot_duration_minutesorg_internalsupport_export
slot_gap_minutesorg_internalsupport_export
cooldown_minutesorg_internalsupport_export
min_lead_time_minutesorg_internalsupport_export
horizon_daysorg_internalsupport_export
slots_open_atorg_internalsupport_export
slots_close_atorg_internalsupport_export
assignment_strategyorg_internalsupport_export
publishedorg_internalsupport_export
published_atsystem_metadatasupport_export
is_publicorg_internalsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

calendar_specialists

ColumnClassEgress
calendar_idsystem_metadatasupport_export
specialist_idsystem_metadatasupport_export
offering_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
priorityorg_internalsupport_export
created_atsystem_metadatasupport_export

calendar_forms

ColumnClassEgress
calendar_idsystem_metadatasupport_export
form_template_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
category_keysystem_metadatasupport_export
category_is_singlesystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
created_atsystem_metadatasupport_export

specialist_assignment_tracking

ColumnClassEgress
calendar_idsystem_metadatasupport_export
specialist_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
last_assigned_atorg_internalsupport_export
assignment_countorg_internalsupport_export

F5 Appointments

appointments

contact_name, contact_email and contact_phone are pii_basic — the same posture as humans.email and organization_invites.email, plaintext per decisions.md → Why most PII is plaintext. All three hold what a person typed when booking, before they onboarded at the clinic; once patient_id links, the authoritative copy is on patient_profiles and these go NULL.

cancellation_reason is clinical_sensitive rather than clinical, and it is the one column here blocked from bulk_export. It is free text a patient or clinician typed about why a medical appointment did not happen; in practice that is where a diagnosis, a bereavement or a symptom lands. The structured cancellation attribution (status, cancelled_at, cancelled_by_principal_id) carries everything an export legitimately needs, so the prose adds risk without adding meaning.

booking_client_id is support_export only. It is a pseudonymous device/browser identifier that keys the public-booking cooldown — a cross-booking correlator by construction, which is precisely why it must not reach an analytics or webhook target.

webhook_egress on appointments (N3, added 2026-08-25). The Cat E events appointment.booked / .rescheduled / .cancelled / .completed / .noshow are the second real producers for the Cat C outbound-webhook framework, and a webhook body is delivered verbatim to a URL the clinic configured — so a payload field is an egress and needs a target here. The columns carrying it are exactly the ones those payloads contain: identifiers, the delivery channel, the instant and duration, and the status with its transition timestamps.

What is deliberately absent is the point. contact_name, contact_email and contact_phone are what a person typed when booking and never travel — a subscriber that needs to reach a patient asks the API with its own credentials and gets whatever its permissions allow, which is where that decision belongs. cancellation_reason is free text where a diagnosis or a bereavement lands. booking_client_id is a cross-booking correlator by construction. And protocol_id / session_id stay out because "this appointment belongs to that course of treatment" is clinical linkage the payload has no need of.

ColumnClassEgress
idsystem_metadatabulk_export, support_export, webhook_egress
organization_idsystem_metadatabulk_export, support_export, webhook_egress
patient_profile_idsystem_metadatabulk_export, support_export, webhook_egress
patient_idsystem_metadatabulk_export, support_export
specialist_idsystem_metadatabulk_export, support_export, webhook_egress
offering_idsystem_metadatabulk_export, support_export, webhook_egress
calendar_idsystem_metadatabulk_export, support_export, webhook_egress
location_idorg_internalbulk_export, support_export
contact_namepii_basicbulk_export, support_export
contact_emailpii_basicbulk_export, support_export
contact_phonepii_basicbulk_export, support_export
booking_client_idsystem_metadatasupport_export
protocol_idclinicalbulk_export, support_export
session_idclinicalbulk_export, support_export
channelclinicalbulk_export, support_export, webhook_egress
scheduled_atclinicalbulk_export, support_export, patient_document, webhook_egress
duration_minutesclinicalbulk_export, support_export, webhook_egress
started_atclinicalbulk_export, support_export
ended_atclinicalbulk_export, support_export
statusclinicalbulk_export, support_export, webhook_egress
cancelled_atclinicalbulk_export, support_export, webhook_egress
cancellation_reasonclinical_sensitivesupport_export
cancelled_by_principal_idsystem_metadatasupport_export
noshow_atclinicalbulk_export, support_export, webhook_egress
noshow_by_principal_idsystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatabulk_export, support_export
updated_atsystem_metadatabulk_export, support_export

appointment_files

Patient- and staff-uploaded documents attached to a consultation — imaging the patient brought, a scan the clinic took, a referral letter.

file_name is clinical_sensitive, and it is the column that decides this table's posture. A filename the patient chose is free text about a medical document, and in practice it is where the diagnosis lands: RMN-hernie-L4-L5.pdf, analize-oncologie.jpg. Treating it as an innocuous label because it is "just metadata" is exactly how a bulk export leaks a diagnosis, so it carries the same class and the same single egress target as appointments.cancellation_reason.

file_url is clinical_sensitive and blocked from every export. It is an S3 object key: not merely a pointer to clinical content but the addressable location of it, and a key that escapes the tenant is a key that outlives every access check the platform makes. Nothing legitimately needs it — an export that should carry the bytes resolves them server-side and embeds them; it does not ship the address and hope.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
appointment_idsystem_metadatabulk_export, support_export
file_urlclinical_sensitive
file_nameclinical_sensitivesupport_export
file_typesystem_metadatabulk_export, support_export
file_sizesystem_metadatabulk_export, support_export
kindsystem_metadatabulk_export, support_export
uploaded_by_principal_idsystem_metadatasupport_export
deleted_atsystem_metadatabulk_export, support_export
created_atsystem_metadatabulk_export, support_export

video_room_participants

Maps the opaque provider-visible participant reference back to a principal.

participant_ref is auth_secret for the same reason room_ref is: it is what the provider was told, and pairing it with a principal here is precisely the correlation the opacity exists to prevent elsewhere. It never leaves.

The table as a whole says "this person was given a way into this consultation", which is why principal_id carries only the support target — a bulk export listing who attended whose consultation is a disclosure about the STAFF, not only the patient.

ColumnClassEgress
video_room_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
participant_refauth_secret
principal_idsystem_metadatasupport_export
first_granted_atsystem_metadatasupport_export

appointment_notes

Staff-only notes on a consultation, including the pre-call technical check.

body is clinical, and the reasoning is the point rather than the class. A technical check is not a clinical act — somebody rang to see whether the camera works. But this is free text a staff member writes ABOUT a patient, and what a person types cannot be bounded: "patient is hard of hearing, use captions" is health information however operational the intent was. Classing it org_internal would be classifying the intent instead of the contents.

STAFF-ONLY IS A VISIBILITY RULE, NOT A DSAR EXEMPTION. The table has no patient RLS branch — deliberately, so a clinic can be candid in its own log — but a subject-access request still reaches personal data held about a patient. Which notes an export carries is an F11 decision, and bulk_export is left empty here to force that decision rather than pre-empt it.

outcome is clinical for a narrower reason: "unreachable" twice before a consultation says something about a patient's circumstances, and it is exactly the field a bulk export would carry without anyone thinking about it.

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
appointment_idsystem_metadatasupport_export
kindsystem_metadatasupport_export
outcomeclinicalsupport_export
bodyclinicalsupport_export
author_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

F5.5 Video Consultations

The substrate for remote consultations (000049): video_rooms is the room that exists, video_session_events is what happened inside it.

video_rooms

Two columns carry the weight, and both are addresses rather than content.

room_ref is auth_secret. It is not clinical data — it is the opaque name that, presented to the provider, admits the bearer to a live medical consultation. That is the same shape as a webhook signing secret, and it takes the same posture: excluded from every egress target, never logged, never serialised. The wire type the API returns has no field for it, so a projection cannot regain the value by someone adding a line — the F5.4 hold-broadcast lesson applied ahead of the leak rather than after it. leo stores the appointment uid here and puts it in URLs and emails, which is what makes G14 a permanent capability rather than a bug.

provider_room_id gets the same treatment for the same reason: with the platform's API key it addresses the same room, and nothing outside the adapter has a use for it.

last_error is org_internal and support-only, with a handling rule the class alone does not express: it is a provider error string, and a provider error string can quote the room name it failed on. Whatever writes it sanitises first. A column that exists to explain a failure must not become the exfiltration path for the secret the failure was about.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
appointment_idsystem_metadatabulk_export, support_export
providersystem_metadatasupport_export
provider_room_idauth_secret
room_refauth_secret
not_before_atsystem_metadatabulk_export, support_export
expires_atsystem_metadatabulk_export, support_export
statussystem_metadatabulk_export, support_export
last_errororg_internalsupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatabulk_export, support_export
updated_atsystem_metadatabulk_export, support_export

video_session_events

The append-only record of a consultation's call. The columns are individually mundane and the table collectively is not: it says this person was in a medical consultation at this time, which is health-adjacent metadata even though no diagnosis appears in it. Patient-facing portability carries it (it is the patient's own attendance) and nothing else does.

payload is clinical_sensitive with no egress target at all, and the reason is that we do not control its shape. It is the provider's raw webhook body, retained for forensics and read by nothing — a third party's blob whose fields can change without notice, and which may well carry a display name the provider derived. A column whose contents are defined by someone else cannot be permitted to leave; classifying it optimistically would be classifying a future version of it.

participant_ref is opaque by construction — it is the reference the platform sent, never a name or an email — which is what lets attribution work without putting patient identity into a third party's event log.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
video_room_idsystem_metadatasupport_export
appointment_idsystem_metadatabulk_export, support_export
providersystem_metadatasupport_export
provider_event_idsystem_metadatasupport_export
event_typesystem_metadatabulk_export, support_export
participant_refsystem_metadatasupport_export
principal_idsystem_metadatasupport_export
occurred_atsystem_metadatabulk_export, support_export
received_atsystem_metadatasupport_export
payloadclinical_sensitive

clinical_captures

One clinician-performed capture during a consultation — today a posture-grid photograph. The bytes live in appointment_files; this row is what they mean.

The table is clinical as a whole even though no column contains a diagnosis: it records that a clinician photographed a patient's body during a consultation, and the existence of that act is health data about a person. The patient's own portability archive carries it; nothing else does.

settings is org_internal rather than clinical. It describes the tool (grid theme, opacity, whether the overlay was drawn), not the patient — the same distinction pdf_templates.editor_state draws between what a document will say about anyone and what is true of one person.

tool_version earns its row in this table for a reason the class does not express: it is the only thing that makes an artefact interpretable later. It is dull metadata that becomes load-bearing the moment anything is derived from a capture.

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
appointment_idsystem_metadatabulk_export, support_export
appointment_file_idsystem_metadatabulk_export, support_export
toolclinicalbulk_export, support_export
tool_versionsystem_metadatabulk_export, support_export
video_room_idsystem_metadatasupport_export
settingsorg_internalsupport_export
captured_by_principal_idsystem_metadatasupport_export
captured_atsystem_metadatabulk_export, support_export
created_atsystem_metadatabulk_export, support_export

F6.1 PDF Templates

The block-based template builder (000047). All three tables are org-scoped configuration; pdf_templates soft-deletes, pdf_template_versions is append-only.

editor_state and its version snapshot are org_internal, and the reasoning matters more than the class. A block list is the clinic's own authoring work — its layout, its letterhead, and above all the rich_text blocks, which are clinician-authored prose (dietary protocols, lab panels, treatment advice) representing real professional effort. It is not clinical: it describes what a document will say about any patient, never what is true of one. No patient value is stored here. The values arrive at render time and land in appointment_documents, where they are classified.

The same distinction governs patient_details block config. A block declaring fields: ["name", "cnp"] states which columns a rendered document will read — a field list, not field values — so it classifies with the template. The values those keys resolve to are decided at render time by classification.AllowedFor and the renderer's own org-scoped query, never by what the template asked for. A template cannot widen its own egress, which is the property that makes storing the field list safe at org_internal.

requires_national_id is org_internal on both the template and its version snapshot, and it exists ONLY here — the form-side twin was removed, because a form field's presence already declares that the form collects a CNP, whereas this flag decides something the layout cannot: whether the value may be decrypted at all. It is a posture flag — "documents from this template may carry a CNP" — and carrying it in version history is what lets a rollback restore the posture along with the blocks, instead of restoring layout under whatever flag happens to be set today.

No webhook_egress anywhere in this section, deliberately. No Cat E event carries a template body, and a block list delivered verbatim to a clinic-configured URL would ship rich_text prose the clinic may never have published to anyone. A subscriber that needs template detail calls the API with its own credentials and receives what its permissions allow.

offering_documents

Pure configuration, and the mirror of offering_forms: which PDF layouts a service produces, and which professional title may generate each. specialist_title_id is the doctor-versus-therapist rule; it names a specialist_titles row and describes no patient.

ColumnClassEgress
offering_idsystem_metadatasupport_export
pdf_template_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
specialist_title_idsystem_metadatasupport_export
sort_ordersystem_metadatasupport_export
created_atsystem_metadatasupport_export

pdf_templates

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
descriptionorg_internalsupport_export
category_idsystem_metadatasupport_export
requires_signaturesystem_metadatasupport_export
editor_stateorg_internalsupport_export
layout_configorg_internalsupport_export
versionsystem_metadatasupport_export
publishedorg_internalsupport_export
published_atorg_internalsupport_export
requires_national_idorg_internalsupport_export
created_by_principal_idsystem_metadatasupport_export
updated_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export

pdf_template_versions

ColumnClassEgress
idsystem_metadatasupport_export
pdf_template_idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
versionsystem_metadatasupport_export
editor_state_snapshotorg_internalsupport_export
layout_config_snapshotorg_internalsupport_export
requires_national_idorg_internalsupport_export
requires_signatureorg_internalsupport_export
published_atorg_internalsupport_export
changed_by_principal_idsystem_metadatasupport_export
change_notesorg_internalsupport_export
created_atsystem_metadatasupport_export

pdf_template_components

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
nameorg_internalsupport_export
descriptionorg_internalsupport_export
blocksorg_internalsupport_export
categorysystem_metadatasupport_export
created_by_principal_idsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

F6.3 Appointment documents

The generated artifact (000048) — a report or medical prescription rendered from a signed form and a frozen pdf_templates version.

document_url is clinical_sensitive and blocked from every export, matching appointment_files.file_url exactly. It is an S3 object key: not merely a pointer to clinical content but the addressable location of it, and a key that escapes the tenant outlives every access check the platform makes. Nothing legitimately needs it — an export that should carry the bytes resolves them server-side and embeds them; it does not ship the address and hope.

title is clinical rather than org_internal. It comes from the template in the ordinary case, but a clinician can name a document, and a title like "Raport evaluare — hernie de disc L4-L5" is a diagnosis written on the outside of the envelope. Classifying it by where it usually comes from rather than by what it can contain is how clinical text ends up in a bulk export.

type, published, pdf_template_version and the supersession pair are system_metadata / org_internal — they describe the document's lifecycle, not its contents. pdf_template_version carries support_export because "which layout produced this" is exactly the question a support investigation asks, and the answer discloses nothing about the patient.

metadata is org_internal and non-clinical by contract — generation diagnostics (render duration, block count, renderer version) and nothing else. A clinical value may never be written here; promote it to a typed column the moment a surface needs one.

optional_choices is that promotion, and it is why it is clinical rather than sitting beside the diagnostics. It records which optional sections (C2) the clinician chose to print and which they were offered and declined, so it describes what a medical document CONTAINS — and a clinic's own label for a section ("Anexă — evaluare psihologică") can say as much about the patient as the section would. support_export only, like title.

No webhook_egress anywhere in this section, deliberately. No Cat E event carries a document, and a clinic-configured URL is not somewhere a patient's report goes. A subscriber that needs one calls the API with its own credentials and receives what its permissions allow.

appointment_documents

ColumnClassEgress
idsystem_metadatabulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
appointment_idsystem_metadatabulk_export, support_export
form_idsystem_metadatabulk_export, support_export
category_keysystem_metadatabulk_export, support_export
pdf_template_idsystem_metadatasupport_export
pdf_template_versionsystem_metadatasupport_export
titleclinicalsupport_export
document_urlclinical_sensitive
publishedorg_internalbulk_export, support_export
published_atorg_internalbulk_export, support_export
generated_by_principal_idsystem_metadatasupport_export
published_by_principal_idsystem_metadatasupport_export
superseded_atsystem_metadatabulk_export, support_export
superseded_by_idsystem_metadatasupport_export
optional_choicesclinicalsupport_export
metadataorg_internalsupport_export
created_atsystem_metadatabulk_export, support_export
updated_atsystem_metadatabulk_export, support_export