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.

How callers use it

Egress paths consult the helper before constructing a payload — never hand-build 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")

The helper parses this doc once at process startup. Malformed or missing entries cause the process to fail to boot — CI catches the malformation before the merge that would deploy it.


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
created_atsystem_metadatasupport_export

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
confirmedorg_internalsupport_export
blockedorg_internalsupport_export
last_activityaudit_onlysupport_export
preferred_languageorg_internalsupport_export
timezoneorg_internalsupport_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
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
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
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
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
treatment_plans_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
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
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
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
human_idsystem_metadatasupport_export
namepii_basicsupport_export
date_of_birthpii_basicsupport_export
sexpii_basicsupport_export
phonepii_basicsupport_export
occupationpii_basicsupport_export
residencepii_basicsupport_export
blood_typeclinicalsupport_export
allergiesclinicalsupport_export
chronic_conditionsclinicalsupport_export
emergency_contact_namepii_basicsupport_export
emergency_contact_phonepii_basicsupport_export
insurance_entriespii_basicsupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

patient_caregivers

ColumnClassEgress
patient_profile_idsystem_metadatasupport_export
caregiver_human_idsystem_metadatasupport_export
relationshippii_basicsupport_export
created_atsystem_metadatasupport_export

patients

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
patient_profile_idsystem_metadatasupport_export
profile_sharedorg_internalsupport_export
consumer_idorg_internalsupport_export
last_used_atsystem_metadatasupport_export
deleted_atsystem_metadatasupport_export
created_atsystem_metadatasupport_export
updated_atsystem_metadatasupport_export

Patient subscriptions

patient_subscriptions

ColumnClassEgress
idsystem_metadatasupport_export
organization_idsystem_metadatasupport_export
patient_idsystem_metadatasupport_export
tier_idsystem_metadatasupport_export
tier_versionorg_internalsupport_export
statusorg_internalsupport_export
started_atsystem_metadatasupport_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

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

Consents

ColumnClassEgress
codepublicbulk_export, support_export
scopepublicbulk_export, support_export
namepublicbulk_export, support_export
descriptionpublicbulk_export, support_export
legal_basispublicbulk_export, support_export
withdrawablepublicbulk_export, support_export
created_atsystem_metadatabulk_export, support_export
ColumnClassEgress
idsystem_metadatabulk_export, support_export
purpose_codepublicbulk_export, support_export
organization_idsystem_metadatabulk_export, support_export
versionpublicbulk_export, support_export
body_translationspublicbulk_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
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 terms of service + privacy notice, assembled from platform templates per 1B.10. Editor state lives in organization_legal_documents; the immutable artefact patients accept is the corresponding row in consent_purpose_versions (already classified above).

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
published_versionsystem_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 + sparse-prefs trio 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

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
scheduled_atsystem_metadatabulk_export, support_export
created_atsystem_metadatabulk_export, support_export

notification_deliveries

ColumnClassEgress
idsystem_metadatabulk_export, support_export
notification_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
read_atsystem_metadatabulk_export, support_export
created_atsystem_metadatabulk_export, support_export

notification_preferences

ColumnClassEgress
recipient_principal_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.

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
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 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 (org_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) or links to external rendering systems (Bunny video IDs, collection IDs). All 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.

exercises

ColumnClassEgress
idsystem_metadatasupport_export
slugorg_internalsupport_export
kindorg_internalsupport_export
statusorg_internalsupport_export
asset_versionsystem_metadatasupport_export
catalog_render_idsystem_metadatasupport_export
bunny_collection_idorg_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
bunny_video_idorg_internalsupport_export
statusorg_internalsupport_export
asset_versionsystem_metadatasupport_export
duration_secondssystem_metadatasupport_export
picksorg_internalsupport_export
rendered_atsystem_metadatasupport_export
failed_reasonorg_internalsupport_export
created_atsystem_metadatasupport_export