-
Notifications
You must be signed in to change notification settings - Fork 0
Data Model Compliance
Tiana_ edited this page May 30, 2026
·
1 revision
Part of the Data Model reference. Schema:
compliance. Companion to Domain-Model, Architecture-Security.
| Table | Role |
|---|---|
compliance.kyc_sessions |
KYC verification session per user (mutable, versioned) |
compliance.kyc_documents |
Document references per KYC session (encrypted external refs) |
compliance.aml_alerts |
AML rule match alerts (append-only) |
compliance.aml_rules |
AML rule definitions and versions |
compliance.compliance_cases |
Investigation cases (mutable, versioned) |
compliance.case_notes |
Append-only notes per case |
compliance.processed_events |
Consumer-side event deduplication |
CREATE TABLE IF NOT EXISTS compliance.kyc_sessions (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
provider VARCHAR(64) NOT NULL,
provider_session_id VARCHAR(255),
hosted_url VARCHAR(2048),
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
rejection_reason VARCHAR(2048),
expires_at TIMESTAMPTZ NOT NULL,
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Encrypted columns (DEK from Vault, AES-256-GCM; see Architecture-Security)
evidence_external_ref BYTEA,
evidence_provider VARCHAR(64),
evidence_verified_at TIMESTAMPTZ,
evidence_confidence NUMERIC(3, 2),
evidence_sanctions_hit BOOLEAN,
evidence_pep_hit BOOLEAN,
evidence_adverse_media BOOLEAN,
CONSTRAINT ck_kyc_sessions_status CHECK (status IN (
'PENDING','AWAITING_DOCUMENTS','IN_REVIEW',
'APPROVED','REJECTED','EXPIRED','MANUAL_REVIEW'
)),
CONSTRAINT ck_kyc_sessions_expires CHECK (expires_at > created_at),
CONSTRAINT ck_kyc_sessions_confidence CHECK (
evidence_confidence IS NULL
OR (evidence_confidence >= 0 AND evidence_confidence <= 1)
)
);
CREATE INDEX IF NOT EXISTS idx_kyc_sessions_user_id
ON compliance.kyc_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_kyc_sessions_status
ON compliance.kyc_sessions(status);
-- Partial: only sessions still awaiting expiry need this lookup
CREATE INDEX IF NOT EXISTS idx_kyc_sessions_expires
ON compliance.kyc_sessions(expires_at)
WHERE status IN ('PENDING','AWAITING_DOCUMENTS');
CREATE OR REPLACE TRIGGER trg_kyc_sessions_updated_at
BEFORE UPDATE ON compliance.kyc_sessions
FOR EACH ROW EXECUTE FUNCTION shared.set_updated_at();Query patterns:
- Load session by ID: PK lookup
- List sessions for user:
idx_kyc_sessions_user_id+ status filter - Expiry batch job:
idx_kyc_sessions_expires(partial, active sessions only)
Notes:
-
provideris a logical identifier; no real KYC provider names are stored in DDL. -
evidence_*columns store results returned from the provider after verification.evidence_external_refis encrypted at the column level (AES-256-GCM, DEK from Vault). See Architecture-Security. -
versionmaps to@Versionfor optimistic locking.
CREATE TABLE IF NOT EXISTS compliance.kyc_documents (
id UUID PRIMARY KEY,
session_id UUID NOT NULL,
type VARCHAR(64) NOT NULL,
external_ref BYTEA NOT NULL, -- encrypted reference to external storage
status VARCHAR(32) NOT NULL DEFAULT 'PENDING',
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_kyc_documents_session FOREIGN KEY (session_id)
REFERENCES compliance.kyc_sessions(id) ON DELETE RESTRICT,
CONSTRAINT ck_kyc_documents_status CHECK (status IN ('PENDING','VERIFIED','REJECTED'))
);
CREATE INDEX IF NOT EXISTS idx_kyc_documents_session
ON compliance.kyc_documents(session_id);Notes:
- No document content is stored in the database.
external_refis an encrypted pointer to the document stored in an external object store. - Append-only for regulatory completeness; no deletes.
CREATE TABLE IF NOT EXISTS compliance.aml_alerts (
id UUID PRIMARY KEY,
transaction_id UUID NOT NULL,
rule_set_id VARCHAR(64) NOT NULL,
risk_score INT NOT NULL,
matched_rules JSONB NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'OPEN',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT ck_aml_alerts_status CHECK (status IN ('OPEN','LINKED_TO_CASE','CLOSED_NO_ACTION')),
CONSTRAINT ck_aml_alerts_risk_score CHECK (risk_score BETWEEN 0 AND 100)
);
CREATE INDEX IF NOT EXISTS idx_aml_alerts_transaction
ON compliance.aml_alerts(transaction_id);
CREATE INDEX IF NOT EXISTS idx_aml_alerts_status
ON compliance.aml_alerts(status);
CREATE INDEX IF NOT EXISTS idx_aml_alerts_created
ON compliance.aml_alerts(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_aml_alerts_risk
ON compliance.aml_alerts(risk_score DESC);Query patterns:
- Load alerts for a transaction:
idx_aml_alerts_transaction - Open alert dashboard (sorted by risk):
idx_aml_alerts_riskfiltered by status - Alert list by recency:
idx_aml_alerts_created
Notes:
- Append-only. Status transitions via update (OPEN -> LINKED_TO_CASE or CLOSED_NO_ACTION).
-
transaction_idreferencesledger.transactionsbut no FK constraint is declared (cross-schema; services may split). Referential integrity is enforced at the application layer.
CREATE TABLE IF NOT EXISTS compliance.aml_rules (
id UUID PRIMARY KEY,
rule_set_id VARCHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
priority INT NOT NULL,
severity VARCHAR(16) NOT NULL,
definition JSONB NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'DRAFT',
version INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by VARCHAR(64) NOT NULL,
activated_at TIMESTAMPTZ,
CONSTRAINT ck_aml_rules_status CHECK (status IN ('DRAFT','ACTIVE','DEPRECATED')),
CONSTRAINT ck_aml_rules_severity CHECK (severity IN ('LOW','MEDIUM','HIGH','CRITICAL'))
);
CREATE INDEX IF NOT EXISTS idx_aml_rules_rule_set_status
ON compliance.aml_rules(rule_set_id, status, priority DESC);Query patterns:
- Load active rules for a rule set (ordered by priority): composite index
(rule_set_id, status, priority DESC), p99 < 10ms with Redis cache
CREATE TABLE IF NOT EXISTS compliance.compliance_cases (
id UUID PRIMARY KEY,
alert_id UUID,
payment_id UUID,
kyc_session_id UUID,
status VARCHAR(32) NOT NULL DEFAULT 'OPEN',
decision VARCHAR(32),
resolved_by VARCHAR(64),
resolved_at TIMESTAMPTZ,
resolution_reason VARCHAR(2048),
priority VARCHAR(16) NOT NULL DEFAULT 'MEDIUM',
assigned_to VARCHAR(64),
ai_explanation TEXT,
draft_report TEXT,
version BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_cases_alert FOREIGN KEY (alert_id)
REFERENCES compliance.aml_alerts(id) ON DELETE RESTRICT,
CONSTRAINT ck_cases_status CHECK (status IN ('OPEN','IN_REVIEW','RESOLVED','ESCALATED')),
CONSTRAINT ck_cases_decision CHECK (
decision IS NULL OR decision IN ('APPROVED','REJECTED','ESCALATED_TO_REGULATOR')
),
CONSTRAINT ck_cases_priority CHECK (priority IN ('LOW','MEDIUM','HIGH','CRITICAL')),
CONSTRAINT ck_cases_at_least_one_link CHECK (
alert_id IS NOT NULL OR payment_id IS NOT NULL OR kyc_session_id IS NOT NULL
),
CONSTRAINT ck_cases_resolution CHECK (
(status = 'RESOLVED' AND decision IS NOT NULL AND resolved_by IS NOT NULL)
OR (status != 'RESOLVED' AND resolved_at IS NULL)
)
);
CREATE INDEX IF NOT EXISTS idx_cases_status
ON compliance.compliance_cases(status);
CREATE INDEX IF NOT EXISTS idx_cases_assigned
ON compliance.compliance_cases(assigned_to) WHERE status = 'IN_REVIEW';
CREATE INDEX IF NOT EXISTS idx_cases_priority
ON compliance.compliance_cases(priority, created_at DESC) WHERE status = 'OPEN';
CREATE OR REPLACE TRIGGER trg_compliance_cases_updated_at
BEFORE UPDATE ON compliance.compliance_cases
FOR EACH ROW EXECUTE FUNCTION shared.set_updated_at();Query patterns:
- Open case queue sorted by priority:
idx_cases_priority(partial, OPEN only) - Cases assigned to investigator:
idx_cases_assigned(partial, IN_REVIEW only) - Case by ID: PK lookup
Notes:
-
ck_cases_resolutionenforces that aRESOLVEDcase always has adecisionandresolved_by, and a non-resolved case cannot haveresolved_atset. -
payment_idandkyc_session_idare plain UUIDs (no cross-schema FK) for the same split-readiness reason as in the payments schema.
CREATE TABLE IF NOT EXISTS compliance.case_notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
case_id UUID NOT NULL,
author_id VARCHAR(64) NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_case_notes_case FOREIGN KEY (case_id)
REFERENCES compliance.compliance_cases(id) ON DELETE RESTRICT
);
CREATE INDEX IF NOT EXISTS idx_case_notes_case
ON compliance.case_notes(case_id, created_at);Notes:
- Append-only audit trail. No updates to note body after creation.
CREATE TABLE IF NOT EXISTS compliance.processed_events (
event_id UUID PRIMARY KEY,
consumer_group VARCHAR(64) NOT NULL,
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_compliance_processed_events_processed
ON compliance.processed_events(processed_at);
-- Cleanup job: DELETE WHERE processed_at < NOW() - INTERVAL '30 days'Notes:
- Records Kafka/Redpanda events already consumed by compliance consumers to enable idempotent processing.
- A mirror table exists in
platform.processed_eventsfor shared/centralized consumers.
| Table | Column | Method |
|---|---|---|
kyc_sessions |
evidence_external_ref |
AES-256-GCM column, DEK from Vault |
kyc_documents |
external_ref |
AES-256-GCM column, DEK from Vault |
All other columns: filesystem-level encryption (LUKS). See Architecture-Security for DEK rotation schedule.
v0.1/
030-compliance-kyc-sessions.xml
031-compliance-kyc-documents.xml
032-compliance-aml-alerts.xml
033-compliance-aml-rules.xml
034-compliance-cases.xml
035-compliance-case-notes.xml
036-compliance-processed-events.xml
- Data-Model - conventions, ERD, cross-cutting sections
- Architecture-Security - encryption at rest, audit
- Domain-Model - KYC and AML aggregate definitions
- Overview
- Services
- Data Model
- Domain Model
- Event Flow
- Security
- Observability
- Resilience
- SLA / SLI / SLO