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 check —
services/api/cmd/check-classificationparses everymigrations/core/*.up.sqland 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 intomake checkand the GitHub Actions PR pipeline. - Runtime helper —
services/api/internal/shared/classification/parses this doc once at startup. Egress paths callclassification.AllowedFor(table, target) []stringandclassification.Filter(record, target) anyto 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.
| Class | Definition | Implications |
|---|---|---|
public | No 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_internal | Settings, 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_basic | Names, 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_regulated | National 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. |
clinical | Diagnoses, 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_sensitive | Mental 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_secret | Credentials 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_only | IPs, 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_metadata | Timestamps, 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.
| Target | Where it applies | Notes |
|---|---|---|
bulk_export | GDPR 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_internal | Telemetry 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_egress | Outbound webhooks (Layer 8) for clinic-installed integrations. Per-event payloads. | Per-org subscription; org-controlled. |
marketing_email | Layer 8 marketing campaigns and transactional notifications that include user-identifying content. | Strict per-patient consent gate (P17) on top of the registry. |
support_export | Break-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:
// 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:
- Regulated identifiers must be encrypted. A column classified
pii_regulatedMUST have typeBYTEAAND a name ending in_encrypted. Catches the case where someone addspassport_number TEXTand classifies itpii_regulated— the build fails until the column is renamed and re-typed (or the class is downgraded with documented reasoning). - The
_encryptedsuffix is reserved. A column whose name ends in_encryptedMUST have typeBYTEAAND classpii_regulatedorauth_secret. Catches the case where someone addsaddress_encrypted BYTEAclassifiedpii_basic— the build fails until the column is renamed (matching the plaintext rule forpii_basic) or the class is upgraded. - Credential hashes are BYTEA. A column whose name ends in
_hashAND class isauth_secretMUST have typeBYTEA. Catchesapi_key_hash TEXTdeclaredauth_secret— SHA-256 belongs inBYTEA, not hex-encoded text.
What's intentionally NOT enforced:
auth_secretcolumns aren't required to beBYTEAacross the board. Cross-system identifiers likehumans.provider_subject_idand short-liveddomain_verification_tokens.tokenare 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, andpubliccolumns have no encryption-related constraints. They rely on the layered controls.- Columns whose name happens to end in
_hashoutside anauth_secretcontext — e.g.,audit_log.inputs_hash(system_metadata, a SHA-256 forensic linker) — are not constrained. The invariant only applies when class isauth_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:
- Decide its class from the table above.
- Decide which egress targets it is explicitly allowed for. Default is none.
- 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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| actor_id | system_metadata | support_export |
| actor_type | audit_only | support_export |
| action | audit_only | support_export |
| entity_type | audit_only | support_export |
| entity_id | audit_only | support_export |
| changes | audit_only | support_export |
| ip_address | audit_only | support_export |
| user_agent | audit_only | support_export |
| request_path | audit_only | support_export |
| request_method | audit_only | support_export |
| status_code | audit_only | support_export |
| request_id | audit_only | support_export |
| action_context | audit_only | support_export |
| break_glass_id | audit_only | support_export |
| impersonation_id | audit_only | support_export |
| created_at | system_metadata | support_export |
audit_ai_provenance
| Column | Class | Egress |
|---|---|---|
| audit_log_id | system_metadata | support_export |
| audit_log_created_at | system_metadata | support_export |
| model_id | audit_only | support_export |
| inputs_hash | audit_only | support_export |
| confidence | audit_only | support_export |
| created_at | system_metadata | support_export |
Identity
principals
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| principal_type | system_metadata | support_export |
| parent_principal_id | system_metadata | support_export |
| created_at | system_metadata | support_export |
| deleted_at | system_metadata | support_export |
humans
| Column | Class | Egress |
|---|---|---|
| principal_id | system_metadata | support_export |
| provider_subject_id | auth_secret | |
| provider_org_id | auth_secret | |
| pii_basic | support_export | |
| confirmed | org_internal | support_export |
| blocked | org_internal | support_export |
| last_activity | audit_only | support_export |
| preferred_language | org_internal | support_export |
| timezone | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
agents
| Column | Class | Egress |
|---|---|---|
| principal_id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| name | org_internal | support_export |
| description | org_internal | support_export |
| model_provider | org_internal | support_export |
| model_name | org_internal | support_export |
| model_version | org_internal | support_export |
| scope | org_internal | support_export |
| system_prompt_ref | org_internal | support_export |
| configuration | org_internal | support_export |
| enabled | org_internal | support_export |
| deleted_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
service_accounts
| Column | Class | Egress |
|---|---|---|
| principal_id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| name | org_internal | support_export |
| description | org_internal | support_export |
| integration_kind | org_internal | support_export |
| api_key_hash | auth_secret | |
| api_key_prefix | org_internal | support_export |
| expires_at | org_internal | support_export |
| last_used_at | audit_only | support_export |
| rotated_at | org_internal | support_export |
| revoked_at | org_internal | support_export |
| deleted_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
platform_memberships
| Column | Class | Egress |
|---|---|---|
| principal_id | system_metadata | support_export |
| role | org_internal | support_export |
| granted_by_principal_id | system_metadata | support_export |
| granted_at | system_metadata | support_export |
RBAC
permissions
| Column | Class | Egress |
|---|---|---|
| code | public | support_export |
| resource | public | support_export |
| action | public | support_export |
| description | public | support_export |
| created_at | system_metadata | support_export |
roles
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| code | org_internal | support_export |
| name | org_internal | support_export |
| description | org_internal | support_export |
| is_system | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
role_permissions
| Column | Class | Egress |
|---|---|---|
| role_id | system_metadata | support_export |
| permission_code | system_metadata | support_export |
| created_at | system_metadata | support_export |
organization_memberships
| Column | Class | Egress |
|---|---|---|
| principal_id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| role_id | system_metadata | support_export |
| is_owner | system_metadata | support_export |
| last_used_at | audit_only | support_export |
| invited_at | system_metadata | support_export |
| invited_by | system_metadata | support_export |
| accepted_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
Organizations & domains
organizations
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| name | public | support_export |
| slug | public | support_export |
| tagline | public | support_export |
| description | public | support_export |
| public | support_export | |
| phone | public | support_export |
| website | public | support_export |
| location | public | support_export |
| logo_url | public | support_export |
| icon_url | public | support_export |
| language_code | org_internal | support_export |
| portal_self_signup_enabled | public | support_export |
| branding | public | support_export |
| tenancy_mode | org_internal | support_export |
| activated_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
organization_domains
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| domain | org_internal | support_export |
| domain_type | org_internal | support_export |
| status | org_internal | support_export |
| verification_token | auth_secret | |
| verified_at | system_metadata | support_export |
| last_check_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
Org settings & companions
organization_settings
| Column | Class | Egress |
|---|---|---|
| organization_id | system_metadata | support_export |
| marketing_email_enabled | org_internal | support_export |
| marketing_sms_enabled | org_internal | support_export |
| audit_retention_months | org_internal | support_export |
| support_locale | org_internal | support_export |
| default_timezone | org_internal | support_export |
| feature_flags | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
organization_billing
| Column | Class | Egress |
|---|---|---|
| organization_id | system_metadata | support_export |
| current_tier_id | org_internal | support_export |
| billing_email | pii_basic | support_export |
| billing_contact_name | pii_basic | support_export |
| billing_address_line1 | pii_basic | support_export |
| billing_address_line2 | pii_basic | support_export |
| billing_city | pii_basic | support_export |
| billing_postal_code | pii_basic | support_export |
| billing_country | pii_basic | support_export |
| tax_id_encrypted | pii_regulated | support_export |
| currency | org_internal | support_export |
| external_customer_id | org_internal | support_export |
| payment_provider | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
organization_entitlements
| Column | Class | Egress |
|---|---|---|
| organization_id | system_metadata | support_export |
| telerehab_enabled | org_internal | support_export |
| treatment_plans_enabled | org_internal | support_export |
| video_consultations_enabled | org_internal | support_export |
| pose_estimation_enabled | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_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.
| Column | Class | Egress |
|---|---|---|
| organization_id | system_metadata | support_export |
| kind | system_metadata | support_export |
| principal_id | system_metadata | support_export |
| external_contact_name | pii_basic | support_export |
| external_contact_email | pii_basic | support_export |
| external_contact_phone | pii_basic | support_export |
| notes | org_internal | support_export |
| assigned_by_principal_id | system_metadata | support_export |
| assigned_at | system_metadata | support_export |
| updated_at | system_metadata | support_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.
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| from_principal_id | system_metadata | support_export |
| to_principal_id | system_metadata | support_export |
| initiated_by_principal_id | system_metadata | support_export |
| accept_token | auth_secret | |
| status | system_metadata | support_export |
| initiated_at | system_metadata | support_export |
| expires_at | system_metadata | support_export |
| resolved_at | system_metadata | support_export |
| initiation_note | org_internal | support_export |
| resolution_note | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
Tiers catalog
tiers
| Column | Class | Egress |
|---|---|---|
| id | public | support_export |
| code | public | support_export |
| name | public | support_export |
| description | public | support_export |
| kind | public | support_export |
| billing_cycle | public | support_export |
| base_price | public | support_export |
| currency | public | support_export |
| version | public | support_export |
| published | public | support_export |
| published_at | public | support_export |
| deprecated_at | public | support_export |
| translations | public | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
tier_versions
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| tier_id | system_metadata | support_export |
| version | public | support_export |
| published_at | public | support_export |
| entitlements_snapshot | public | support_export |
| limits_snapshot | public | support_export |
| metadata_snapshot | public | support_export |
| changed_by_principal_id | org_internal | support_export |
| created_at | system_metadata | support_export |
entitlements
| Column | Class | Egress |
|---|---|---|
| code | public | support_export |
| name | public | support_export |
| description | public | support_export |
| regulated | public | support_export |
| entitlement_column | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
limit_definitions
| Column | Class | Egress |
|---|---|---|
| code | public | support_export |
| name | public | support_export |
| description | public | support_export |
| unit | public | support_export |
| default_behavior | public | support_export |
| period_kind | public | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
tier_entitlements
| Column | Class | Egress |
|---|---|---|
| tier_id | public | support_export |
| entitlement_code | public | support_export |
| enabled | public | support_export |
| created_at | system_metadata | support_export |
tier_limits
| Column | Class | Egress |
|---|---|---|
| tier_id | public | support_export |
| limit_code | public | support_export |
| cap_value | public | support_export |
| behavior_override | public | support_export |
| created_at | system_metadata | support_export |
Org subscriptions
organization_subscriptions
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| tier_id | org_internal | support_export |
| tier_version | org_internal | support_export |
| status | org_internal | support_export |
| started_at | org_internal | support_export |
| current_period_starts_at | org_internal | support_export |
| current_period_ends_at | org_internal | support_export |
| cancel_at | org_internal | support_export |
| canceled_at | org_internal | support_export |
| payment_provider | org_internal | support_export |
| external_subscription_id | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
organization_subscription_entitlements
| Column | Class | Egress |
|---|---|---|
| subscription_id | system_metadata | support_export |
| entitlement_code | org_internal | support_export |
| enabled | org_internal | support_export |
| created_at | system_metadata | support_export |
organization_subscription_limits
| Column | Class | Egress |
|---|---|---|
| subscription_id | system_metadata | support_export |
| limit_code | org_internal | support_export |
| cap_value | org_internal | support_export |
| behavior | org_internal | support_export |
| created_at | system_metadata | support_export |
organization_subscription_overrides
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| subscription_id | system_metadata | support_export |
| override_kind | org_internal | support_export |
| entitlement_code | org_internal | support_export |
| entitlement_enabled | org_internal | support_export |
| limit_code | org_internal | support_export |
| cap_value | org_internal | support_export |
| behavior_override | org_internal | support_export |
| granted_by_principal_id | org_internal | support_export |
| reason | org_internal | support_export |
| effective_from | org_internal | support_export |
| expires_at | org_internal | support_export |
| revoked_at | org_internal | support_export |
| revoked_by_principal_id | org_internal | support_export |
| created_at | system_metadata | support_export |
Patient tiers
patient_tiers
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| code | org_internal | support_export |
| name | org_internal | support_export |
| description | org_internal | support_export |
| is_active | org_internal | support_export |
| is_default | org_internal | support_export |
| sort_order | org_internal | support_export |
| version | org_internal | support_export |
| published | org_internal | support_export |
| published_at | org_internal | support_export |
| external_price_hint | org_internal | support_export |
| currency | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
patient_tier_versions
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| tier_id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| version | org_internal | support_export |
| published_at | org_internal | support_export |
| entitlements_snapshot | org_internal | support_export |
| limits_snapshot | org_internal | support_export |
| metadata_snapshot | org_internal | support_export |
| changed_by_principal_id | system_metadata | support_export |
| created_at | system_metadata | support_export |
patient_tier_entitlements
| Column | Class | Egress |
|---|---|---|
| tier_id | system_metadata | support_export |
| entitlement_code | org_internal | support_export |
| enabled | org_internal | support_export |
| created_at | system_metadata | support_export |
patient_tier_limits
| Column | Class | Egress |
|---|---|---|
| tier_id | system_metadata | support_export |
| limit_code | org_internal | support_export |
| cap_value | org_internal | support_export |
| behavior_override | org_internal | support_export |
| created_at | system_metadata | support_export |
Patient identity
patient_profiles
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| human_id | system_metadata | support_export |
| name | pii_basic | support_export |
| date_of_birth | pii_basic | support_export |
| sex | pii_basic | support_export |
| phone | pii_basic | support_export |
| occupation | pii_basic | support_export |
| residence | pii_basic | support_export |
| blood_type | clinical | support_export |
| allergies | clinical | support_export |
| chronic_conditions | clinical | support_export |
| emergency_contact_name | pii_basic | support_export |
| emergency_contact_phone | pii_basic | support_export |
| insurance_entries | pii_basic | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
patient_caregivers
| Column | Class | Egress |
|---|---|---|
| patient_profile_id | system_metadata | support_export |
| caregiver_human_id | system_metadata | support_export |
| relationship | pii_basic | support_export |
| created_at | system_metadata | support_export |
patients
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| patient_profile_id | system_metadata | support_export |
| profile_shared | org_internal | support_export |
| consumer_id | org_internal | support_export |
| last_used_at | system_metadata | support_export |
| deleted_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
Patient subscriptions
patient_subscriptions
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| patient_id | system_metadata | support_export |
| tier_id | system_metadata | support_export |
| tier_version | org_internal | support_export |
| status | org_internal | support_export |
| started_at | system_metadata | support_export |
| current_period_starts_at | org_internal | support_export |
| current_period_ends_at | org_internal | support_export |
| cancel_at | org_internal | support_export |
| canceled_at | org_internal | support_export |
| payment_provider | org_internal | support_export |
| external_subscription_id | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
patient_subscription_entitlements
| Column | Class | Egress |
|---|---|---|
| subscription_id | system_metadata | support_export |
| entitlement_code | org_internal | support_export |
| enabled | org_internal | support_export |
| created_at | system_metadata | support_export |
patient_subscription_limits
| Column | Class | Egress |
|---|---|---|
| subscription_id | system_metadata | support_export |
| limit_code | org_internal | support_export |
| cap_value | org_internal | support_export |
| behavior | org_internal | support_export |
| created_at | system_metadata | support_export |
patient_subscription_overrides
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| subscription_id | system_metadata | support_export |
| override_kind | org_internal | support_export |
| entitlement_code | org_internal | support_export |
| entitlement_enabled | org_internal | support_export |
| limit_code | org_internal | support_export |
| cap_value | org_internal | support_export |
| behavior_override | org_internal | support_export |
| granted_by_principal_id | org_internal | support_export |
| reason | org_internal | support_export |
| effective_from | org_internal | support_export |
| expires_at | org_internal | support_export |
| revoked_at | org_internal | support_export |
| revoked_by_principal_id | org_internal | support_export |
| created_at | system_metadata | support_export |
Consents
consent_purposes
| Column | Class | Egress |
|---|---|---|
| code | public | bulk_export, support_export |
| scope | public | bulk_export, support_export |
| name | public | bulk_export, support_export |
| description | public | bulk_export, support_export |
| legal_basis | public | bulk_export, support_export |
| withdrawable | public | bulk_export, support_export |
| created_at | system_metadata | bulk_export, support_export |
consent_purpose_versions
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| purpose_code | public | bulk_export, support_export |
| organization_id | system_metadata | bulk_export, support_export |
| version | public | bulk_export, support_export |
| body_translations | public | bulk_export, support_export |
| published_at | system_metadata | bulk_export, support_export |
| published_by_principal_id | system_metadata | support_export |
| created_at | system_metadata | bulk_export, support_export |
consents
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| organization_id | system_metadata | bulk_export, support_export |
| patient_profile_id | pii_basic | bulk_export, support_export |
| purpose_code | org_internal | bulk_export, support_export |
| purpose_version | system_metadata | bulk_export, support_export |
| source | audit_only | bulk_export, support_export |
| source_form_id | system_metadata | bulk_export, support_export |
| granted_at | audit_only | bulk_export, support_export |
| granted_by_principal_id | pii_basic | bulk_export, support_export |
| granted_via_ip | audit_only | support_export |
| withdrawn_at | audit_only | bulk_export, support_export |
| withdrawn_by_principal_id | pii_basic | bulk_export, support_export |
| withdrawal_reason | pii_basic | bulk_export, support_export |
| created_at | system_metadata | bulk_export, support_export |
Legal documents
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).
legal_document_templates
Platform catalog. Bodies are public-by-design (the same way consent_purposes is) — they're the scaffolding clinics fill in, not clinic-specific data.
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| document_type | public | bulk_export, support_export |
| version | public | bulk_export, support_export |
| locale | public | bulk_export, support_export |
| body_with_placeholders | public | bulk_export, support_export |
| required_placeholders | public | bulk_export, support_export |
| toggleable_sections | public | bulk_export, support_export |
| published_at | system_metadata | bulk_export, support_export |
| published_by_principal_id | system_metadata | support_export |
| created_at | system_metadata | bulk_export, support_export |
organization_legal_documents
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.
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| organization_id | system_metadata | bulk_export, support_export |
| document_type | public | bulk_export, support_export |
| source_template_version | public | bulk_export, support_export |
| placeholder_values | org_internal | bulk_export, support_export |
| included_sections | org_internal | bulk_export, support_export |
| published_version | system_metadata | bulk_export, support_export |
| published_against_template_version | system_metadata | bulk_export, support_export |
| last_reviewed_by_principal_id | system_metadata | support_export |
| last_reviewed_at | audit_only | bulk_export, support_export |
| created_at | system_metadata | bulk_export, support_export |
| updated_at | system_metadata | bulk_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| organization_id | system_metadata | bulk_export, support_export |
| recipient_principal_id | system_metadata | bulk_export, support_export |
| recipient_email | pii_basic | bulk_export, support_export |
| category | org_internal | bulk_export, support_export |
| idempotency_key | system_metadata | support_export |
| locale | org_internal | bulk_export, support_export |
| timezone | org_internal | bulk_export, support_export |
| subject | pii_basic | bulk_export, support_export |
| body_text | pii_basic | bulk_export, support_export |
| body_html | pii_basic | bulk_export, support_export |
| scheduled_at | system_metadata | bulk_export, support_export |
| created_at | system_metadata | bulk_export, support_export |
notification_deliveries
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| notification_id | system_metadata | bulk_export, support_export |
| channel | org_internal | bulk_export, support_export |
| status | org_internal | bulk_export, support_export |
| attempts | system_metadata | support_export |
| claimed_at | system_metadata | support_export |
| claimed_by_worker_id | system_metadata | support_export |
| next_attempt_at | system_metadata | support_export |
| sent_at | system_metadata | bulk_export, support_export |
| provider_message_id | system_metadata | support_export |
| last_error | org_internal | support_export |
| read_at | system_metadata | bulk_export, support_export |
| created_at | system_metadata | bulk_export, support_export |
notification_preferences
| Column | Class | Egress |
|---|---|---|
| recipient_principal_id | system_metadata | bulk_export, support_export |
| category | org_internal | bulk_export, support_export |
| channel | org_internal | bulk_export, support_export |
| enabled | org_internal | bulk_export, support_export |
| updated_at | system_metadata | bulk_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| principal_id | system_metadata | bulk_export, support_export |
| organization_id | system_metadata | bulk_export, support_export |
| scope | org_internal | bulk_export, support_export |
| reason_category | org_internal | bulk_export, support_export |
| reason_text | pii_basic | support_export |
| reason_ref | org_internal | support_export |
| opened_at | system_metadata | bulk_export, support_export |
| expires_at | system_metadata | bulk_export, support_export |
| closed_at | system_metadata | bulk_export, support_export |
| closed_by_principal_id | system_metadata | bulk_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | bulk_export, support_export |
| staff_principal_id | system_metadata | bulk_export, support_export |
| target_patient_id | system_metadata | bulk_export, support_export |
| organization_id | system_metadata | bulk_export, support_export |
| reason | pii_basic | support_export |
| opened_at | system_metadata | bulk_export, support_export |
| expires_at | system_metadata | bulk_export, support_export |
| closed_at | system_metadata | bulk_export, support_export |
| closed_by_principal_id | system_metadata | bulk_export, support_export |
Personal invitations + patient share-links
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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| provider_invitation_id | system_metadata | support_export |
| pii_basic | support_export | |
| kind | org_internal | support_export |
| role_id | system_metadata | support_export |
| patient_tier_id | system_metadata | support_export |
| invited_by_principal_id | system_metadata | support_export |
| invited_at | system_metadata | support_export |
| expires_at | system_metadata | support_export |
| accepted_at | system_metadata | support_export |
| accepted_principal_id | system_metadata | support_export |
| consumed_at | system_metadata | support_export |
| revoked_at | system_metadata | support_export |
| revoked_by_principal_id | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
share_links
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| code | org_internal | support_export |
| kind | org_internal | support_export |
| patient_tier_id | system_metadata | support_export |
| max_uses | org_internal | support_export |
| use_count | org_internal | support_export |
| expires_at | system_metadata | support_export |
| note | org_internal | support_export |
| created_by_principal_id | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
| revoked_at | system_metadata | support_export |
| revoked_by_principal_id | system_metadata | support_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| slug | public | support_export |
| name | public | support_export |
| timezone | org_internal | support_export |
| phone | org_internal | support_export |
| org_internal | support_export | |
| address_line1 | pii_basic | support_export |
| address_line2 | pii_basic | support_export |
| city | pii_basic | support_export |
| county | pii_basic | support_export |
| postal_code | pii_basic | support_export |
| country | pii_basic | support_export |
| status | org_internal | support_export |
| closed_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| capability | org_internal | support_export |
| organization_id | system_metadata | support_export |
| provider_name | org_internal | support_export |
| credentials_encrypted | auth_secret | |
| config | org_internal | support_export |
| status | org_internal | support_export |
| last_error_at | org_internal | support_export |
| last_error | org_internal | support_export |
| last_health_check_at | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| target_url | org_internal | support_export |
| signing_secret_encrypted | auth_secret | |
| signing_secret_previous_encrypted | auth_secret | |
| signing_secret_rotated_at | system_metadata | support_export |
| event_filters | org_internal | support_export |
| status | org_internal | support_export |
| failure_count | system_metadata | support_export |
| last_success_at | system_metadata | support_export |
| last_failure_at | system_metadata | support_export |
| created_by_principal_id | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
outbound_webhook_deliveries
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| subscription_id | system_metadata | support_export |
| event_id | system_metadata | support_export |
| event_name | org_internal | support_export |
| payload | org_internal | support_export |
| status | system_metadata | support_export |
| attempt_count | system_metadata | support_export |
| next_attempt_at | system_metadata | support_export |
| claimed_at | system_metadata | support_export |
| claimed_by_worker_id | system_metadata | support_export |
| last_attempt_at | system_metadata | support_export |
| last_response_status_code | system_metadata | support_export |
| last_response_body | org_internal | support_export |
| dead_lettered_at | system_metadata | support_export |
| created_at | system_metadata | support_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
| Column | Class | Egress |
|---|---|---|
| id | public | support_export |
| slug | public | support_export |
| name | public | support_export |
| description | public | support_export |
| auth_type | public | support_export |
| oauth_scopes | public | support_export |
| oauth_client_capability | system_metadata | support_export |
| icon_url | public | support_export |
| status | public | support_export |
| config_schema | public | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
organization_integrations
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| integration_service_id | system_metadata | support_export |
| auth_type | org_internal | support_export |
| external_account_id | org_internal | support_export |
| title | org_internal | support_export |
| status | org_internal | support_export |
| oauth_expires_at | system_metadata | support_export |
| credentials_encrypted | auth_secret | |
| config | org_internal | support_export |
| last_used_at | system_metadata | support_export |
| last_error_at | system_metadata | support_export |
| last_error | org_internal | support_export |
| created_by_principal_id | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_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
| Column | Class | Egress |
|---|---|---|
| provider | system_metadata | support_export |
| event_id | system_metadata | support_export |
| processed_at | system_metadata | support_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| capability | system_metadata | support_export |
| units | system_metadata | support_export |
| unit_type | system_metadata | support_export |
| cost_cents | system_metadata | support_export |
| principal_id | system_metadata | support_export |
| occurred_at | system_metadata | support_export |
| metadata | system_metadata | support_export |
usage_quotas
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| capability | system_metadata | support_export |
| period | system_metadata | support_export |
| limit_units | system_metadata | support_export |
| current_units | system_metadata | support_export |
| period_start_at | system_metadata | support_export |
| period_end_at | system_metadata | support_export |
| last_reset_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
usage_summaries
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| organization_id | system_metadata | support_export |
| capability | system_metadata | support_export |
| period | system_metadata | support_export |
| period_start_at | system_metadata | support_export |
| period_end_at | system_metadata | support_export |
| total_units | system_metadata | support_export |
| total_cost_cents | system_metadata | support_export |
| calls_count | system_metadata | support_export |
| created_at | system_metadata | support_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| model_provider | org_internal | support_export |
| model_name | org_internal | support_export |
| model_version | org_internal | support_export |
| capability | org_internal | support_export |
| unit_type | system_metadata | support_export |
| validation_status | org_internal | support_export |
| validation_notes | org_internal | support_export |
| status | org_internal | support_export |
| introduced_at | system_metadata | support_export |
| retired_at | system_metadata | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
ai_model_pricing_history
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | |
| model_id | system_metadata | |
| cost_per_input_unit_cents | audit_only | |
| cost_per_output_unit_cents | audit_only | |
| effective_from | audit_only | |
| effective_to | audit_only | |
| changed_by_principal_id | audit_only | |
| notes | audit_only | |
| created_at | system_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
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| slug | org_internal | support_export |
| kind | org_internal | support_export |
| status | org_internal | support_export |
| asset_version | system_metadata | support_export |
| catalog_render_id | system_metadata | support_export |
| bunny_collection_id | org_internal | support_export |
| created_at | system_metadata | support_export |
| updated_at | system_metadata | support_export |
exercise_renders
| Column | Class | Egress |
|---|---|---|
| id | system_metadata | support_export |
| exercise_id | system_metadata | support_export |
| recipe_hash | system_metadata | support_export |
| language | org_internal | support_export |
| recipe | org_internal | support_export |
| bunny_video_id | org_internal | support_export |
| status | org_internal | support_export |
| asset_version | system_metadata | support_export |
| duration_seconds | system_metadata | support_export |
| picks | org_internal | support_export |
| rendered_at | system_metadata | support_export |
| failed_reason | org_internal | support_export |
| created_at | system_metadata | support_export |