Data schema reference
This document is the table-by-table reference for the Find My Data persistence layer, written for engineers and DBAs who need to read, extend, or debug the database. It covers every table created by migrations0001–0012 in packages/server/migrations/, grouped by migration, and ends with an entity-relationship overview of the most central tables. As of release 26.7.15.0 the schema is a single SQLite database accessed only through role-scoped repository interfaces (ADR-0003); see architecture for how the modules sit on top of it and the threat model for the security assumptions the schema encodes.
Conventions
- IDs — every primary key is
TEXT: sortable, opaque, generated by the kernel (packages/server/src/kernel/ids.ts). - Times — every timestamp is
INTEGERUTC epoch milliseconds (created_at,observed_at,expires_at, …). - JSON columns — columns holding serialized JSON end in
_json; where they have a default, it is'[]'or'{}'. - Tenancy — every business table carries a
NOT NULL tenant_id REFERENCES tenants(id), and unique indexes are tenant-prefixed (with a few deliberate exceptions such as join-table composite keys and per-parent uniques likedataset_membersandrelease_evaluations). Two deliberate exceptions:scanners(deployment-level infrastructure, not tenant data) and thesearch_ftsvirtual table (tenant scoping is enforced by joining throughsearch_documents). - Deny-by-default reads — the schema assumes the authz kernel (
packages/server/src/kernel/authz.ts): access is denied unless a role/ownership rule allows it, and UI hiding is never the enforcement mechanism. Rows do not carry per-row ACLs; scoping columns (tenant_id,domain_id) are the enforcement inputs. - Temporal validity — slowly-changing facts use
valid_from/valid_to(valid_to IS NULL= current) or anis_currentflag with a partial index (WHERE is_current = 1). Observations are appended, never rewritten. - Provenance and status enums — most governed rows carry a
provenance(e.g.ModelInferred) and astatuslifecycle enforced byCHECKconstraints. AI output is never silently authoritative: statuses likeProposedByAI→ValidatedByGRCmake the human step explicit (ADR-0006). - Rebuildable projections —
graph_nodes,graph_edges,search_documents, andsearch_ftsare derived read models. They can be dropped and rebuilt from the base tables at any time; nothing authoritative lives in them. - Honesty in enums — enums include values for what could not be observed (
not_observable,partial,unknown,not_enabled) rather than defaulting to an optimistic value.
Migration 0001 — tenancy, identity, governance taxonomy
0001_identity_governance.sql. The multi-tenant spine, identity/principal model, and the human-governed taxonomy (domains, information types, classifications, mappings, Purview labels).
tenants
Root of all tenancy. Everything else hangs offtenants.id.
org_profiles
One row per organization profile used to seed taxonomy generation:name, industries_json, geographies_json, description, ai_provider (default 'mock'). status CHECK: draft | approved. FK: tenant_id.
principals
Unified identity table for humans and non-humans.kind CHECK: user | group | service | broad_audience | sharing_link. Identity columns: issuer/subject (IdP claims, users), external_id (source directory id), display_name, email, title, department. source CHECK: fixture | entra | system; lifecycle_status CHECK: active | disabled | deleted.
Unique (partial) indexes: (tenant_id, issuer, subject) where issuer IS NOT NULL; (tenant_id, external_id) where external_id IS NOT NULL.
group_memberships
Observed group edges:group_id and member_id both FK principals(id). kind CHECK: direct | transitive; completeness CHECK: complete | partial | not_observable (membership expansion may be unobservable and the schema says so). Unique: (tenant_id, group_id, member_id, kind).
role_assignments
Application roles per principal.role CHECK: platform_admin | governance_admin | domain_owner | remediation_approver | auditor. Unique: (tenant_id, principal_id, role). These roles are the input to deny-by-default authorization (see permissions manifest).
sessions
Server sessions:principal_id, auth_mode CHECK dev | entra (ADR-0004), csrf_token, expires_at, nullable revoked_at.
generation_runs
Provenance record for every AI generation:task, provider, model, template_version, input_hash, status CHECK completed | failed. Taxonomy rows point back here via generation_run_id.
policy_documents / policy_sections
Uploaded governance policy documents (title, filename, content_hash, version, status CHECK active | superseded, uploaded_by FK principals) and their ordered sections (document_id FK, section_ref e.g. "4.2", heading, body, ord). Mappings can cite a policy_section_id as their justification.
business_domains
Business domains of the taxonomy:name (unique per tenant), description, inclusion/exclusion guidance, status CHECK draft | approved | retired, version, provenance (default 'ModelInferred'), optional generation_run_id.
classifications
Sensitivity classifications:name (unique per tenant), rank (INTEGER, higher = more sensitive), status CHECK draft | approved | retired, provenance.
information_types
The core taxonomy unit, owned by a domain (domain_id FK business_domains). Columns: definition, examples_json, counterexamples_json, sensitivity_rationale, expected_classification_id FK classifications, expected_location_note, status CHECK draft | approved | retired, version, provenance, generation_run_id. Unique: (tenant_id, name).
sensitive_categories / info_type_categories
Category vocabulary (name unique per tenant) and its many-to-many join to information types (composite PK (information_type_id, category_id)).
data_elements / info_type_elements
Atomic data elements (name unique per tenant, aliases_json, optional detector_key linking to a detector) and their many-to-many join to information types (composite PK (information_type_id, data_element_id)).
regulatory_obligations
Named obligations (name unique per tenant, jurisdiction, source_ref). Linked to taxonomy rows via governance_mappings.
governance_mappings
Versioned polymorphic many-to-many relationships with a review lifecycle:subject_kind/subject_id → relation → object_kind/object_id, plus rationale, optional policy_section_id citation, confidence REAL, provenance. status CHECK: ProposedByAI | PolicyDerived | ValidatedByGRC | Rejected | Superseded | NeedsReview; reviewer_id FK principals records who validated.
ownership_assignments
Who owns/stewards which domain (optionally narrowed to an information type).kind CHECK: owner | delegate | steward; status CHECK: pending | active | expired | revoked. Temporal validity via valid_from/valid_to; attestation via attested_at/attestation_due_at. FKs: principal_id, domain_id, information_type_id, assigned_by (all principal/taxonomy references).
sensitivity_labels
Purview sensitivity label catalog per Microsoft tenant:ms_tenant_id, label_guid, name, priority, active. catalog_source CHECK: fixture | get_label_import | graph_beta | manual — the schema records how the catalog was obtained. Unique: (tenant_id, ms_tenant_id, label_guid).
classification_label_mappings
Maps internal classifications to Purview labels:direction CHECK classification_to_label | label_to_classification | bidirectional; status CHECK ProposedByAI | ValidatedByGRC | Rejected | Superseded. Unique: (tenant_id, classification_id, label_id).
Migration 0002 — source topology and durable jobs
0002_source_and_jobs.sql. Connector configuration, the observed M365 file topology (scopes → containers → assets → versions/locations), deduplicated permissions, and the durable scan-job machinery (ADR-0007, ADR-0008).
connector_instances
A configured connector.connector_type CHECK: m365 (only value today); mode CHECK: mock | live — mock is the default/CI path; the live Graph connector was validated read-only against the CDX tenant (ADR-0013, CDX runbook). config_json holds non-secret config only; secret_ref is an opaque reference into env/Key Vault and never a secret value. status CHECK: configured | active | error | disabled.
source_scopes
A scannable scope (a drive).kind CHECK: sp_drive | onedrive; external_scope_id is the provider drive id. status CHECK active | paused | removed; coverage_state CHECK none | inventory_running | baseline | delta | stale | error tracks scan freshness. expected_domain_id FK business_domains records the governed location expectation. Unique: (tenant_id, connector_id, external_scope_id).
source_cursors
Provider pagination/delta tokens per scope.cursor_kind CHECK: inventory_next | delta; value is the opaque provider token stored verbatim, with checksum and version. status CHECK: valid | invalid | resync_required. Unique: (tenant_id, scope_id, cursor_kind).
source_containers
Site/drive/folder hierarchy inside a scope:kind CHECK site | drive | folder, self-referencing parent_id, path, and an expected_domain_id governance expectation. Unique: (tenant_id, scope_id, external_id).
physical_assets
The stable identity of a file across renames/moves.kind CHECK: file; status CHECK: active | deleted (soft delete with deleted_at). Unique: (tenant_id, scope_id, external_id).
asset_versions
One row per observed content version of an asset.
Unique:
(tenant_id, asset_id, source_version).
asset_locations
Where an asset lives over time:container_id FK, name, path, with temporal validity valid_from/valid_to (valid_to IS NULL = current; partial indexes on current rows).
permission_sets / permission_grants
Deduplicated permission representation.permission_sets stores one row per distinct normalized grant set: digest (hash of normalized grants, unique per tenant), broad_categories_json (e.g. ["tenant_wide_group","anonymous_link"]), principal_count, has_external, completeness CHECK complete | partial | not_observable. permission_grants holds the members of a set: nullable principal_id (or broad_category when the grant is a broad audience rather than a principal), role (read/write/owner), inheritance CHECK direct | inherited, link_scope (anonymous/organization/specific for sharing links).
asset_permission_obs
Observation joining an asset(+version) to a permission set at a point in time:acquisition_context (default 'app_read_identity'), completeness CHECK as above, observed_at, is_current flag with partial index.
asset_label_obs
Observed sensitivity-label state per asset version: nullablelabel_id FK sensitivity_labels, state CHECK labeled | unlabeled | unknown | unsupported | inaccessible, method CHECK metadata | extract_api | fixture, is_current flag. Live label reads are feature-gated (FMD_FEATURE_PURVIEW_LABEL_READ); with the gate off the honest state is unknown.
activity_aggregates
Windowed activity per asset:window_start/window_end, reads/edits/shares counters, last_activity_at, editor_principal_ids_json. source_state CHECK: observed | not_enabled | partial — absence of an activity feed is recorded, not guessed.
scan_campaigns
A durable scan job.kind CHECK: inventory | delta | rescore; scope_id is NULL for rescore campaigns. params_json is the immutable request; status CHECK pending | running | paused | completed | cancelled | failed; requested_by FK principals. progress_total_est is NULL when unknown — never fabricated.
work_units
Leased queue items within a campaign.stage_attempts
Per-stage execution log:work_unit_id (a work_units.id, or a synthetic ref like propagate:<assetId> for decision-driven re-scores — deliberately not an FK), asset_id, stage, cache_key, outcome CHECK computed | cache_hit | failed | skipped, plus impl_version/config_version for reproducibility.
stage_cache
Stage result cache: presence of a row under(tenant_id, cache_key) (unique) means the stage output is reusable. result_json holds safe summaries/result references only.
dead_letters
Work units that exhausted retries:work_unit_id FK, retry_class, safe_diagnostic (redacted — no paths/names/content), replayed_at when re-queued.
outbox_events
Transactional outbox for projection/propagation events:event_type, payload_json, partition_key, processed_at (NULL = pending; partial index on unprocessed rows).
Migration 0003 — intelligence and review
0003_intelligence_review.sql. Derived artifacts, fingerprints, features, detector/ensemble releases, the evidence → assertion chain (ADR-0006), clusters, risk, findings, and the owner review loop.
artifacts
Derived blobs stored outside the DB.kind CHECK: extracted_text_bounded | excerpt | feature_blob | dataset | report; path is relative under the FMD_ARTIFACT_DIR object store; content_hash, size_bytes. classification defaults 'sensitive_derivative'; use_flags_json is an allowlist — absence of a flag means the use is not permitted; retention_class defaults 'standard' (ADR-0011).
fingerprints / fingerprint_bands
Content fingerprints per asset version:representation CHECK raw_bytes | normalized_text; algorithm CHECK sha256 | simhash64 with algo_version. Unique: (tenant_id, asset_version_id, representation, algorithm, algo_version). fingerprint_bands stores SimHash band values (16 bands of 4 bits each; band_no, band_value) for near-duplicate candidate retrieval, indexed on (tenant_id, band_no, band_value).
feature_artifacts
Extracted features per asset version:kind CHECK keyphrases | detector_hits | language | excerpts | structure; versioned by algo_version + config_version + input_hash. Small features inline in value_json; large ones referenced via artifact_id. status CHECK: valid | incompatible. Unique: (tenant_id, asset_version_id, kind, algo_version, config_version).
detector_releases / ensemble_releases
Versioned detector definitions (detector_key + version unique per tenant; kind CHECK pattern | dictionary | keyphrase | context | near_dup | model_adapter; definition_json; status active | retired) and versioned ensemble configurations (config_json with weights/thresholds/detector versions, compat_feature_versions_json, status active | superseded, version unique per tenant).
evidence
Atomic observations that support or contradict a conclusion. Polymorphic subject:subject_kind CHECK asset | asset_version | container | principal + subject_id. kind examples (comment-documented): phrase_hit, near_duplicate, label_state, exposure, editor_context, location_context, policy_citation, human_decision, keyphrase_overlap. direction CHECK supports | contradicts; weight REAL; value_json bounded safe values only; optional artifact_id; release_ref names the detector/ensemble release. access_classification CHECK: metadata | evidence_summary | evidence_excerpt — the evidence-access gate uses this. Both observed_at and recorded_at are kept.
assertions
Machine conclusions with full provenance and supersession chains.clusters / cluster_members
Asset groupings:kind CHECK exact_duplicate | near_duplicate | context, with algorithm/algo_version, optional representative_asset_id, status active | superseded. Members carry score REAL and reason; unique (tenant_id, cluster_id, asset_id).
risk_assessments
Scored risk per subject (subject_kind CHECK asset | container | domain): policy_version, components_json (array of {component, raw, normalized, evidenceRefs, note}), total_score REAL, severity CHECK low | moderate | elevated | high, coverage_note, is_current flag with partial index.
findings
Actionable issues surfaced to owners.kind examples: overexposure, unexpected_location, label_gap, rot_candidate, duplicate_sprawl. dedup_key unique per tenant prevents duplicate findings; title uses calm, precise language (no breach vocabulary). severity CHECK as above; status CHECK: open | acknowledged | planned | remediating | resolved | reopened | suppressed. References: domain_id, subject_refs_json, assertion_ids_json, risk_id; lifecycle times first_seen_at/last_seen_at/resolved_at.
review_candidates
The owner review queue: one row per assertion routed for review (assertion_id unique per tenant), scoped by domain_id, pointing at asset_id, ranked by priority REAL and uncertainty REAL (with reprioritized_reason). status CHECK: pending | decided | deferred | escalated | stale.
review_decisions
Owner decisions on candidates.decision CHECK: mine | mine_different_type | another_domain | mine_not_sensitive | not_a_match | need_more_evidence; optional corrected_information_type_id and routed_domain_id; batch_id; training_eligible flag; resulting_assertion_id links to the assertion the decision produced.
dataset_candidates
Decisions promoted toward training data:decision_id FK, kind CHECK positive | counterexample, status CHECK pending | approved | rejected. Consumed by migration 0008.
Migration 0004 — analyst, actions, audit
0004_analyst_actions_audit.sql. Analyst query runs, the remediation action state machine (ADR-0010), and the tamper-evident audit log.
analyst_queries
One row per analyst question (ADR-0009):question, planner CHECK deterministic | ai, plan_json, validation_state CHECK valid | rejected | narrowed, scope_json (server-side injected tenant/domain scope), executed_desc_json, result_row_refs_json, coverage_json, model_ref, answer_text, status CHECK answered | rejected | error.
action_requests
The remediation state machine.kind CHECK: internal_task | purview_label_set | purview_label_remove. status CHECK: draft | validated | awaiting_approval | approved | executing | verifying | succeeded | partially_succeeded | failed | cancelled | drifted | blocked. Targets: finding_id, domain_id, target_asset_id, and target_snapshot_version_id (the asset version at preview time — drift is detected against it). current_state_json / desired_state_json capture the planned mutation; approval_required defaults 1; idempotency_key unique per tenant. Live Purview label writes are feature-gated (FMD_FEATURE_PURVIEW_LABEL_WRITE) and run under a separate remediation identity; the mock path is the validated default.
action_approvals
Approval decisions:approver_id FK principals, decision CHECK approve | reject | request_changes, reason.
action_attempts
Execution attempts:attempt_no (unique (tenant_id, action_id, attempt_no)), executor_identity (which workload identity executed — read vs remediation), idempotency_key, status CHECK running | succeeded | failed | skipped_duplicate (duplicate delivery yields one mutation), source_correlation, error_category, result_json.
action_verifications
Post-execution source verification:observed_state_json and outcome CHECK verified | mismatch | failed | unsupported.
audit_events
Append-only audit with a per-tenant hash chain for tamper evidence:seq unique per tenant, prev_hash + hash chain each event to its predecessor. actor_id is NULL for system actions. category values (comment-documented): auth, config, governance, scan, evidence_access, review, analyst, action, model, operations. outcome CHECK: success | denied | failure; policy_decision; details_safe_json contains allowlisted fields only; correlation_id ties events to requests.
Migration 0005 — rebuildable projections
0005_projections.sql. Derived read models for graph and search. Everything in this migration is rebuildable from base tables; treat it as a cache with indexes, not a source of truth.
graph_nodes / graph_edges
Typed property graph. Nodes:kind (documented vocabulary: asset, container, domain, information_type, principal, permission_set, cluster, finding, label), ref_id (id of the underlying record; unique (tenant_id, kind, ref_id)), label_safe (display text safe for the owner evidence level), props_json. Edges: from_node_id/to_node_id FK graph_nodes, typed kind (no generic RELATED_TO), and a required human-readable reason. Unique: (tenant_id, from_node_id, to_node_id, kind).
search_documents
Search projection metadata and the security facet: one row per asset ((tenant_id, asset_id) unique) carrying domain_id (domain scoping for search authorization happens here), information_type_id, container_path, facets_json, freshness_at.
search_fts (FTS5 virtual table)
tenant_id column of its own — queries must join doc_id back to search_documents where tenant and domain scoping are enforced. Rebuildable like the rest of this migration.
projection_watermarks
Rebuild bookkeeping: composite PK(tenant_id, projection) where projection is graph or search, with watermark_at and version.
Migration 0006 — scanner fleet
0006_scanner_fleet.sql. Distributed scanner workers.
scanners
Deployment-level infrastructure — the one table withouttenant_id. A registered worker that pulls work from the shared queue via outbound polling. id is the workload identity and doubles as work_units.lease_owner. Columns: display_name, host (pseudonymous host/process id), capabilities_json (connector types handled, e.g. ["m365"]), version, status CHECK active | draining | stale | offline, registered_at, last_heartbeat_at (indexed; abandoned work is reclaimed on lease expiry), processed_count, failed_count.
Migration 0007 — change notifications
0007_change_notifications.sql. Graph change-notification subscriptions and received notifications. Notifications are treated as hints to reconcile via the delta feed, never as authoritative content. Built and validated with the dev simulator (a simulated notification drove a real reconcile); live Graph subscriptions additionally require a public HTTPS webhook URL and the FMD_FEATURE_CHANGE_NOTIFICATIONS flag.
change_subscriptions
Subscription lifecycle:scope_id/connector_id FKs, resource (e.g. /drives/{id}/root), change_type (default 'updated'), external_subscription_id (Graph subscription id, live mode only). client_state_hash stores only the sha256 of the per-subscription clientState — the raw value is never stored; the receiver recomputes the expected value with an HMAC. created_by FK principals records the standing intent (reconcile campaigns run as this principal). mode CHECK mock | live; status CHECK pending | active | expiring | expired | removed | error; expires_at, last_renewed_at, last_error (safe category only). Unique: (tenant_id, resource, mode).
change_notifications
Received notifications:subscription_id FK, resource, change_type, freshness points source_event_at (provider-reported) and received_at, reconciliation linkage reconcile_campaign_id FK scan_campaigns + reconciled_at, dedup_key (resource+changeType within a short window). outcome CHECK: received | deduplicated | reconcile_enqueued | reconcile_skipped | reconciled | rejected; reject_reason is a safe category only. source CHECK: live | simulated — simulated events are labeled as such.
Migration 0008 — model governance
0008_model_governance.sql. Turns owner review feedback into curated, split, consented datasets and gates ensemble/detector releases behind evaluation plus human approval.
dataset_snapshots
Curated dataset versions:name unique per tenant, consent_scope CHECK tenant_internal (default) | vendor_shareable — sharing beyond the tenant requires an explicit customer choice. split_strategy (default 'cluster_aware'), counts (positive_count, counterexample_count, train_count, eval_count), created_by FK principals.
dataset_members
Snapshot membership:candidate_id FK dataset_candidates (unique per snapshot), denormalized asset_id and information_type_id, label CHECK positive | counterexample, split CHECK train | eval, cluster_key (cluster id or singleton asset id — makes the split leakage-safe), model_confidence at snapshot time.
model_releases
Proposed releases:kind CHECK ensemble | detector (with target_key when detector), proposed version + config_json, base_version + base_config_json (rollback snapshot), dataset_snapshot_id FK. status CHECK: proposed | evaluated | approved | promoted | rejected | rolled_back. Separation of duties: decided_by must differ from proposed_by (enforced in application code; both FK principals). ensemble_release_id FK ensemble_releases is set on promote.
release_evaluations
Evaluation results per release and variant (variant CHECK candidate | current; unique (release_id, variant)): confusion counts (true_pos, false_pos, true_neg, false_neg), precision, recall, f1, threshold.
Migration 0009 — embeddings
0009_embeddings.sql. Semantic embeddings with an approximate (LSH-bucketed) candidate index. Embeddings are a retained feature: computed once per asset version and re-scored without any source I/O.
asset_embeddings
One embedding per asset version and model:model (e.g. fmd-hash-256-v1), dim (vectors stored inline as JSON for dim ≤ 256), vector_json (L2-normalized float array), signature (LSH signature, hex), content_version. Unique: (tenant_id, asset_version_id, model).
embedding_buckets
One row per (embedding, band bucket) for candidate retrieval by shared band:embedding_id FK, denormalized asset_id, model, band ("bandIndex:value"), indexed on (tenant_id, model, band).
Migration 0010 — Teams connector
0010_teams.sql. Team/channel topology, channel messages, and file canonicalization. Built and mock-validated; the live path is feature-gated behind FMD_FEATURE_TEAMS_CONNECTOR. A channel’s files live in the team’s SharePoint site, so attachments are canonicalized to the backing driveItem (already inventoried by the SharePoint connector) rather than re-ingested as duplicate assets.
teams_teams
Observed teams per connector:external_team_id (unique per (tenant_id, connector_id)), display_name, visibility CHECK private | public | unknown.
teams_channels
Channels:team_id FK, external_channel_id (unique per team), membership_type CHECK standard | private | shared | unknown. Backing storage linkage: backing_scope_external_id (SharePoint drive id), backing_folder_path, and backing_scope_id FK source_scopes when the drive is known locally; expected_domain_id FK business_domains.
teams_messages
Channel messages — a distinct modality with its own table:external_message_id (unique per channel), parent_message_id (reply threading, external id), sender fields, created_at_source, body_preview (bounded, redaction-safe preview only), body_hash, detector_hits_json (detector keys only, no raw content), has_attachments.
teams_attachment_refs
A message attachment pointing at a SharePoint driveItem:external_drive_id/external_item_id, file_name, canonical_asset_id FK physical_assets (the resolved SharePoint asset — a duplicate asset is never created), resolution CHECK canonicalized | pending | external. Unique: (tenant_id, message_id, external_item_id).
Migration 0011 — Exchange connector
0011_exchange.sql. Selected mailboxes, per-folder message delta, and attachment↔SharePoint matching. Built and mock-validated; the live path is feature-gated behind FMD_FEATURE_EXCHANGE_CONNECTOR. Key distinction from Teams: an email attachment is a copy of a file, not the SharePoint item itself, so a match is recorded as a similarity relationship — never merged into one identity.
exchange_mailboxes
Selected mailboxes per connector:external_mailbox_id (UPN/user id; unique per (tenant_id, connector_id)), display_name. access_scope CHECK: application_access_policy (default — least privilege via an application access policy, not tenant-wide Mail.Read) | tenant_wide | fixture.
exchange_folders
Mail folders with per-folder delta state:mailbox_id FK, external_folder_id (unique per mailbox), delta_cursor (opaque per-folder delta token).
exchange_messages
Messages:folder_id FK, external_message_id (unique per folder), subject, sender fields, received_at_source, body_preview (bounded, redaction-safe), body_hash, detector_hits_json, has_attachments, importance (default 'normal').
exchange_attachments
Attachments and their similarity match:external_attachment_id (unique per message), file_name, size_bytes, content_type, content_hash (sha256 of raw bytes when available), normalized_text_hash (sha256 of normalized extracted text, indexed for matching). similar_asset_id FK physical_assets records a similarity to a SharePoint asset (a distinct copy, not the same identity); match_method CHECK: identical_content | identical_text | near_duplicate | filename_size | none; match_confidence REAL.
Migration 0012 — control plane
0012_control_plane.sql. SaaS control-plane state. The control plane handles licensing and fleet health only — never customer content or sensitive evidence.
control_plane_state
One row per tenant (PKtenant_id): telemetry_enabled (default 1; customer-controlled, opt-outable at any time), last_entitlement_json (cached last-valid entitlement for the offline grace window; non-secret), last_verified_at, updated_at.
Entity-relationship overview
The diagram shows the 17 most central tables. Every table also referencestenants via tenant_id (edges omitted for readability), and evidence/assertions use polymorphic subject_kind/subject_id references rather than declared FKs — those are drawn as logical links labeled “subject”.
Related reading: architecture (module layout over this schema), threat model (what the safe-diagnostic/redaction columns defend against), capacity model (row-count expectations), permissions manifest (roles referenced by role_assignments), and ADR-0003 (why a single SQLite database behind role interfaces).