Data Model: Security & Compliance
On this page
Overview
The security service is CRAIG’s compliance, audit, and platform-integrity backbone (port 8007,
database craig_security). Its tables back the audit trail (45 CFR § 95.621), the security-review
and NIST SP 800-53 control registries, data archiving/retention (2 CFR § 200.334), the
administrative-unit registry, breach detection (rules + alerts), partner + worker identity
(ADR-017 /
ADR-018 / ADR-026), and the
outbox/inbox/dead-letter durability + idempotency machinery
(ADR-022). This page catalogs all
19 tables (#1173 originally counted 17; the epic &77 event_inbox_collisions
request_claims and epic &78 schema_compat_floor arrived since, and the orphaned
idempotency_responses was dropped by &77 BF; source of truth:
services/craig-security/migrations/). The schema carries
exactly four foreign keys — review_evidence.review_id → security_reviews.id,
security_alerts.rule_id → detection_rules.id, partner_api_keys.partner_id → partners.id,
and partner_signer_keys.partner_id → partners.id; the durability tables' cross-table links
are logical, by envelope_id.
The endpoint catalog for this service is API: Security & Compliance;
its state machines are in State Machines; its events are in
ADR-003: RabbitMQ Topology (the wildcard # audit
subscriber).
Tables
| Table | Purpose |
|---|---|
|
Append-only audit trail of all data access + mutation (45 CFR § 95.621); the wildcard |
|
Biennial / incident / ad-hoc security reviews (45 CFR § 95.621(f)) — findings + remediation plans; soft-deletable. |
|
NIST SP 800-53 control mapping + implementation-status tracking; soft-deletable, partial-unique
on |
|
Data-archiving + purge tracking (2 CFR § 200.334) — retention windows + purge eligibility per source service/table. |
|
Registry of administrative units (states / counties / districts) with jurisdiction + FIPS
mappings; partial-unique on |
|
Breach-detection rule registry (six pattern types over |
|
Alerts fired by breached detection rules — FK to |
|
Forensic capture of RabbitMQ DLX-dead-lettered messages for causal-chain reconstruction |
|
Transactional outbox for this service’s own staged events (ADR-022), published-only 30-day retention (ADR-058). |
|
Idempotent inbox for subscription handlers — the ADR-062 single-transaction attempt machine (claim + effects + stamp atomic; supersedes ADR-022 §D3.2’s three-statement shape), 31-day retention (ADR-058). |
|
ADR-062 §B convert-class idempotency claims (claim-first inside the domain transaction);
pruned hourly on the fleet-wide |
|
ADR-063 rollback floor singleton — the minimum binary version the schema still serves; destructive contract migrations bump it in the same file. Written only by the migration gate; the verify-only boot SELECTs it. |
|
Identity-collision ledger (ADR-062 §A-4): reused envelope ids with divergent
|
|
Partner organization registry (hospital / school / LE / other) for edge identity (ADR-017);
|
|
SHA-256-hashed partner API credentials (FK → |
|
Per-PERSON ES256 signer keys for mandated-reporter detached JWS (ADR-018; FK → |
|
IdP-neutral worker identity cache (ADR-026) — lazy-populated from JWT claims by the auth-after middleware; trigram autocomplete for BFF reassignment. |
|
Uploaded evidence objects attached to security reviews (FK → |
|
CCWIS major-change monitoring (staffing / org / system / policy) with ACF-reporting flags; soft-deletable. |
Key columns
Source of truth: services/craig-security/migrations/. Every PK is id UUID DEFAULT uuidv7()
unless noted (ADR-002).
audit_log
| Column | Type | Notes |
|---|---|---|
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() |
|
TEXT |
acting worker identity (display) |
|
TEXT |
originating service |
|
TEXT |
e.g. read / create / update / delete |
|
TEXT |
domain resource class |
|
UUID |
nullable |
|
JSONB |
nullable structured context |
|
TEXT |
nullable |
|
BOOLEAN |
NOT NULL, DEFAULT true |
|
TEXT |
NOT NULL, DEFAULT 'unknown' — on-behalf-of service (ADR-028) |
|
UUID |
nullable — on-behalf-of subject |
security_reviews
| Column | Type | Notes |
|---|---|---|
|
TEXT |
NOT NULL — the closed |
|
DATE |
NOT NULL |
|
DATE |
nullable |
|
TEXT |
NOT NULL |
|
JSONB |
nullable |
|
TEXT |
NOT NULL, DEFAULT 'scheduled' |
|
BOOLEAN / TIMESTAMPTZ |
soft-delete (active partial index) |
nist_controls
| Column | Type | Notes |
|---|---|---|
|
TEXT |
NOT NULL, partial-UNIQUE while |
|
TEXT |
NOT NULL |
|
TEXT |
NOT NULL, DEFAULT 'planned' |
|
TEXT |
nullable |
|
TEXT |
nullable (object-store pointer) |
|
DATE |
nullable |
|
TEXT |
nullable (sub + denormalized display name) |
|
BOOLEAN / TIMESTAMPTZ |
soft-delete |
archive_records
#1129 (ADR-058): the durable LOCAL LEDGER of the archive-then-prune engine — one row per archive
batch, written inside the batch’s prune transaction (D8). Also holds craig-rules' fleet
BOOKKEEPING copies (validated from rules.evaluations_archived, D16) — those are outside
security’s purge authority (rules' own archive_ledger drives their lifecycle).
| Column | Type | Notes |
|---|---|---|
|
TEXT |
NOT NULL — what was archived |
|
INTEGER |
NOT NULL |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() |
|
DATE |
NULLABLE since #1129: NULL = awaiting the DFCS records-retention schedule (⁂ #1073); purge refuses NULL/future/held at EVERY destructive statement (D14) |
|
BOOLEAN |
NOT NULL, DEFAULT false — purge-time audit fact ONLY (set true at purge; the D14 predicate is the purge authority). #1145 retired the list filter, the web badge, and the |
|
TIMESTAMPTZ / TEXT |
the D15-attributed purge stamp (partial index WHERE purged_at IS NULL) |
|
TEXT |
actor sub or |
|
BOOLEAN |
NOT NULL, DEFAULT false — never purged while true (D14) |
|
TEXT |
full object keys (NULL only on pre-engine legacy rows) |
|
TEXT |
sha256 of the data object’s exact bytes |
|
TEXT |
the D9 store identity at archive time |
|
UUID |
the batch’s [min, max] row-id range (D17 candidate lookup; min/max never inverts) |
|
TIMESTAMPTZ |
oldest/newest archived row by the age column |
|
INTEGER |
the window in force when the batch was cut |
admin_unit_registry
| Column | Type | Notes |
|---|---|---|
|
TEXT |
NOT NULL |
|
TEXT |
NOT NULL (state / county / district) |
|
TEXT |
nullable |
|
TEXT |
NOT NULL |
|
TEXT |
NOT NULL — partial-UNIQUE |
|
BOOLEAN |
NOT NULL, DEFAULT true |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() |
detection_rules
| Column | Type | Notes |
|---|---|---|
|
TEXT |
NOT NULL, UNIQUE |
|
TEXT |
NOT NULL |
|
TEXT |
NOT NULL, CHECK — failed_auth / bulk_access / after_hours_access / privilege_escalation / data_export / account_lockout |
|
INTEGER |
NOT NULL — fire when the window count ≥ threshold |
|
TEXT |
NOT NULL, CHECK (info / warning / critical) |
|
BOOLEAN |
NOT NULL, DEFAULT true — scan-participation switch |
|
TEXT |
nullable — critical-severity push target |
|
BOOLEAN |
NOT NULL, DEFAULT true — soft-delete flag |
security_alerts
rule_id → detection_rules.id is one of the schema’s four foreign keys (see Overview). #1172:
uq_security_alerts_standing_rule (partial UNIQUE on rule_id WHERE NOT acknowledged)
DB-enforces the #1156 one-standing-alert-per-rule invariant — the scan’s insert uses it as an
explicit ON CONFLICT arbiter, so overlapping scans produce exactly one alert. Legacy
NULL-rule_id rows are outside the constraint (partial-index NULLs are distinct).
| Column | Type | Notes |
|---|---|---|
|
UUID |
FK → |
|
TEXT |
NOT NULL (severity CHECK: info / warning / critical) |
|
JSONB |
nullable — window count context |
|
BOOLEAN |
NOT NULL, DEFAULT false — partial-unique key domain |
|
TEXT |
nullable — operator sub, or the attributed |
|
TIMESTAMPTZ |
nullable |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() |
dead_letter_audit
| Column | Type | Notes |
|---|---|---|
|
UUID |
NOT NULL — correlates to |
|
TEXT |
NOT NULL |
|
TEXT |
NOT NULL |
|
JSONB |
NOT NULL — the full dead-lettered message |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() |
|
TEXT |
NULLABLE (#1181): per-occurrence idempotency token ( |
|
TIMESTAMPTZ |
NULLABLE (#1181): when the occurrence happened (when derivable); the #1156 threshold window keys on |
event_inbox
PK is envelope_id UUID (the dedup key from the envelope), not a generated id.
| Column | Type | Notes |
|---|---|---|
|
UUID |
PK — dedup key |
|
TEXT |
NOT NULL |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() |
|
TIMESTAMPTZ |
nullable — set when the handler returns Ok |
|
INT |
NOT NULL, DEFAULT 0 |
|
TEXT |
nullable |
|
TIMESTAMPTZ |
nullable — permanently failed (DLX-surfaced only after a CONFIRMED surface, ADR-062) |
|
TIMESTAMPTZ |
nullable — in-flight marker |
|
TEXT |
nullable — hex SHA-256 of the payload, the third leg of the ADR-062 identity triple (#1053); NULL = legacy pre-migration claim (backfilled at the next success stamp; NOT NULL ratchet #1273) |
event_inbox_collisions
Present in ALL 8 stateful services (like event_inbox; documented here per the transport-table
catalog convention). A reused envelope id whose (source_service, event_type, payload_hash)
triple diverges from the canonical event_inbox claim is recorded here and nacked without
requeue (ADR-062 §A-4, #1053); the canonical row is never mutated. Any row is an alert — the
report-only inbox_identity_collisions_present watchdog fires on presence.
| Column | Type | Notes |
|---|---|---|
|
UUID |
part of the composite PK — the reused id |
|
TEXT |
NOT NULL — the collider’s identity |
|
TEXT |
NOT NULL — part of the composite PK with the two above |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() — the deterministic |
|
INT |
NOT NULL, DEFAULT 1 — repeat observations of the same collider bump this |
partners
| Column | Type | Notes |
|---|---|---|
|
TEXT |
NOT NULL |
|
TEXT |
NOT NULL, CHECK (hospital / school / le / other) |
|
TEXT |
NOT NULL, DEFAULT 'active', CHECK (active / suspended / deleted) — suspended blocks |
|
TEXT |
nullable |
|
INTEGER |
NOT NULL, DEFAULT 60, CHECK > 0 — mirrors intake’s per-partner default |
|
UUID |
NOT NULL |
partner_api_keys
Plaintext keys are never stored — the raw key is returned exactly once at issue time. SHA-256
(not argon2/bcrypt) is deliberate: keys are 256-bit high-entropy random tokens, not passwords,
and the hash must support the indexed hot-path /partners/verify lookup (partial index on
unrevoked key_hash).
| Column | Type | Notes |
|---|---|---|
|
UUID |
FK → |
|
TEXT |
NOT NULL, UNIQUE — SHA-256 hex of the raw key |
|
TEXT |
NOT NULL — first 8 chars, safe to display for revocation UX |
|
TEXT |
nullable |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() + 1 year (F-009 rotation) — expired keys fail verify even if unrevoked |
|
TIMESTAMPTZ |
nullable |
|
UUID |
NOT NULL |
partner_signer_keys
ADR-018: one partner carries many signer keys, ONE PER PERSON — mandatory-reporting statute identifies the individual as the accountable reporting party. Scoped to the partner (not the api-key) so credential rotation never orphans signer keys.
| Column | Type | Notes |
|---|---|---|
|
UUID |
FK → |
|
TEXT |
NOT NULL — the person (email, employee id, …) |
|
TEXT |
NOT NULL |
|
JSONB |
NOT NULL (RFC 7517) |
|
TEXT |
NOT NULL, DEFAULT + CHECK 'ES256' |
|
TEXT |
NOT NULL, UNIQUE — the JWS header |
|
TEXT |
NOT NULL, DEFAULT 'pending', CHECK (pending / approved / revoked) — admin review gates approval |
|
UUID / TIMESTAMPTZ |
nullable approval stamp |
|
UUID / TIMESTAMPTZ |
nullable revocation stamp |
|
TIMESTAMPTZ |
nullable |
|
UUID |
NOT NULL |
worker_identities
PK is sub UUID (the IdP subject), not a generated id. Lazy-upserted from JWT claims by the
auth-after middleware on every authenticated craig-security request (ADR-026).
| Column | Type | Notes |
|---|---|---|
|
UUID |
PK — IdP subject |
|
TEXT |
NOT NULL — deliberately non-unique ( |
|
TEXT |
NOT NULL — trigram-indexed for |
|
TEXT |
nullable |
|
JSONB |
NOT NULL, DEFAULT '{}' — custom IdP claims (assigned_units / region / …) for per-jurisdiction authz expressions, no schema change needed |
|
TIMESTAMPTZ |
NOT NULL, DEFAULT now() |
review_evidence
| Column | Type | Notes |
|---|---|---|
|
UUID |
NOT NULL, FK → |
|
TEXT |
NOT NULL |
|
TEXT |
NOT NULL — object-store pointer (bytes live in the store, never here) |
|
TEXT |
NOT NULL |
major_changes
| Column | Type | Notes |
|---|---|---|
|
TEXT |
NOT NULL, CHECK (worker_assignment / supervisor_change / org_restructure / system_upgrade / policy_change) |
|
TEXT |
NOT NULL |
|
DATE |
NOT NULL |
|
BOOLEAN |
NOT NULL, DEFAULT false |
|
DATE |
nullable |
|
TEXT |
NOT NULL / nullable (sub + denormalized display name) |
|
BOOLEAN / TIMESTAMPTZ |
soft-delete |
Related
-
API: Security & Compliance — the endpoint catalog.
-
ADR-022 — the outbox/inbox/DLQ durability model
event_inbox+dead_letter_auditimplement. -
ADR-028 — the on-behalf-of identity the
actor_*audit columns capture.