Database Schema¶
Verity uses two database engines: PostgreSQL 16 with TimescaleDB for operational data and ClickHouse for the immutable audit log and compliance analytics.
PostgreSQL + TimescaleDB¶
Entity-Relationship Diagram¶
erDiagram
PEER_GROUPS ||--o{ PRINCIPALS : "has members"
PRINCIPALS ||--o{ ACCESS_GRANTS : "owns"
ASSETS ||--o{ ACCESS_GRANTS : "target of"
ACCESS_GRANTS ||--o{ ACCESS_SCORES : "scored by"
ACCESS_GRANTS ||--o{ ACCESS_EVENTS : "generates"
ACCESS_GRANTS ||--o{ ACCESS_STATS_LATEST : "summarised in"
ACCESS_GRANTS ||--o{ REVIEW_PACKETS : "reviewed via"
REVIEW_PACKETS ||--o{ REVIEW_DECISIONS : "decided by"
PRINCIPALS ||--o{ CONNECTOR_CHECKPOINTS : "tracked per connector"
PEER_GROUPS {
uuid id PK
text name
text description
text platform
jsonb criteria
timestamptz created_at
timestamptz updated_at
}
PRINCIPALS {
uuid id PK
text external_id
text platform
text email
text display_name
text principal_type
uuid peer_group_id FK
text status
jsonb metadata
timestamptz first_seen
timestamptz last_seen
timestamptz created_at
timestamptz updated_at
}
ASSETS {
uuid id PK
text external_id
text platform
text asset_type
text name
text sensitivity
text data_classification
jsonb metadata
timestamptz created_at
timestamptz updated_at
}
ACCESS_GRANTS {
uuid id PK
uuid principal_id FK
uuid asset_id FK
text platform
text privilege_level
text grant_type
text status
timestamptz granted_at
timestamptz expires_at
timestamptz last_used_at
jsonb metadata
timestamptz created_at
timestamptz updated_at
}
ACCESS_SCORES {
uuid grant_id FK
timestamptz scored_at
float decay_score
float time_factor
float peer_factor
float privilege_factor
float sensitivity_factor
float anomaly_factor
float compliance_factor
text model_version
}
ACCESS_SCORES_LATEST {
uuid grant_id FK
timestamptz bucket
float avg_decay_score
float max_decay_score
float last_decay_score
}
ACCESS_EVENTS {
uuid id PK
uuid grant_id FK
uuid principal_id FK
uuid asset_id FK
text platform
text event_type
text action
text source_ip
jsonb event_metadata
timestamptz occurred_at
timestamptz ingested_at
}
ACCESS_STATS_LATEST {
uuid grant_id FK
bigint total_events
bigint events_last_30d
bigint events_last_90d
timestamptz last_event_at
timestamptz computed_at
}
REVIEW_PACKETS {
uuid id PK
uuid grant_id FK
uuid principal_id FK
uuid asset_id FK
text status
float trigger_score
text trigger_reason
uuid assigned_to
text workflow_run_id
timestamptz created_at
timestamptz updated_at
timestamptz due_at
}
REVIEW_DECISIONS {
uuid id PK
uuid review_id FK
text decision
text justification
uuid decided_by
text decided_by_email
timestamptz decided_at
jsonb metadata
}
CONNECTOR_CHECKPOINTS {
uuid id PK
text connector_name
text platform
text checkpoint_type
text checkpoint_value
jsonb metadata
timestamptz created_at
timestamptz updated_at
}
Table Details¶
peer_groups¶
Logical groupings of principals used for peer-comparison scoring.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
name |
text |
NO | — | Human-readable group name. |
description |
text |
YES | — | Optional description. |
platform |
text |
NO | — | Source platform (e.g. azure_ad, snowflake). |
criteria |
jsonb |
YES | '{}' |
Dynamic membership criteria. |
created_at |
timestamptz |
NO | now() |
Row creation timestamp. |
updated_at |
timestamptz |
NO | now() |
Last modification timestamp. |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_peer_groups |
id |
PRIMARY KEY |
ix_peer_groups_platform |
platform |
B-tree |
principals¶
Identity records — users, service accounts, and groups from connected platforms.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
external_id |
text |
NO | — | Platform-native identifier. |
platform |
text |
NO | — | Source platform. |
email |
text |
YES | — | Email address (if available). |
display_name |
text |
YES | — | Human-readable name. |
principal_type |
text |
NO | 'user' |
One of: user, service_account, group. |
peer_group_id |
uuid |
YES | — | FK → peer_groups.id. |
status |
text |
NO | 'active' |
One of: active, inactive, suspended. |
metadata |
jsonb |
YES | '{}' |
Platform-specific attributes. |
first_seen |
timestamptz |
YES | — | First time seen by a connector. |
last_seen |
timestamptz |
YES | — | Most recent connector observation. |
created_at |
timestamptz |
NO | now() |
Row creation timestamp. |
updated_at |
timestamptz |
NO | now() |
Last modification timestamp. |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_principals |
id |
PRIMARY KEY |
uq_principals_ext |
(external_id, platform) |
UNIQUE |
ix_principals_email |
email |
B-tree |
ix_principals_peer_group |
peer_group_id |
B-tree |
ix_principals_status |
status |
B-tree |
assets¶
Resources whose access is being tracked — databases, workspaces, applications, etc.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
external_id |
text |
NO | — | Platform-native identifier. |
platform |
text |
NO | — | Source platform. |
asset_type |
text |
NO | — | Type (e.g. database, workspace, application). |
name |
text |
YES | — | Human-readable name. |
sensitivity |
text |
YES | 'medium' |
Sensitivity level: low, medium, high, critical. |
data_classification |
text |
YES | — | Classification label (e.g. public, internal, confidential, restricted). |
metadata |
jsonb |
YES | '{}' |
Platform-specific attributes. |
created_at |
timestamptz |
NO | now() |
Row creation timestamp. |
updated_at |
timestamptz |
NO | now() |
Last modification timestamp. |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_assets |
id |
PRIMARY KEY |
uq_assets_ext |
(external_id, platform) |
UNIQUE |
ix_assets_type |
asset_type |
B-tree |
ix_assets_sensitivity |
sensitivity |
B-tree |
access_grants¶
The core entity — an access relationship between a principal and an asset.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
principal_id |
uuid |
NO | — | FK → principals.id. |
asset_id |
uuid |
NO | — | FK → assets.id. |
platform |
text |
NO | — | Source platform. |
privilege_level |
text |
NO | — | Privilege tier: read, write, admin, owner. |
grant_type |
text |
NO | 'direct' |
How access was granted: direct, inherited, group. |
status |
text |
NO | 'active' |
Status: active, revoked, expired, under_review. |
granted_at |
timestamptz |
YES | — | When access was originally granted. |
expires_at |
timestamptz |
YES | — | Expiration date (if time-boxed). |
last_used_at |
timestamptz |
YES | — | Last time the access was exercised. |
metadata |
jsonb |
YES | '{}' |
Platform-specific grant attributes. |
created_at |
timestamptz |
NO | now() |
Row creation timestamp. |
updated_at |
timestamptz |
NO | now() |
Last modification timestamp. |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_access_grants |
id |
PRIMARY KEY |
ix_grants_principal |
principal_id |
B-tree |
ix_grants_asset |
asset_id |
B-tree |
ix_grants_status |
status |
B-tree |
ix_grants_platform |
platform |
B-tree |
uq_grants_natural |
(principal_id, asset_id, platform, privilege_level) |
UNIQUE |
access_scores — TimescaleDB Hypertable¶
Time-series table storing every decay score computation. Configured as a TimescaleDB hypertable with 7-day chunks.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
grant_id |
uuid |
NO | — | FK → access_grants.id. |
scored_at |
timestamptz |
NO | now() |
Timestamp of scoring run (hypertable partition key). |
decay_score |
double precision |
NO | — | Composite decay score (0.0 – 1.0). |
time_factor |
double precision |
NO | — | Time-since-last-use factor. |
peer_factor |
double precision |
NO | — | Peer-comparison factor. |
privilege_factor |
double precision |
NO | — | Privilege-level weight. |
sensitivity_factor |
double precision |
NO | — | Resource-sensitivity weight. |
anomaly_factor |
double precision |
NO | — | Anomaly-detection signal. |
compliance_factor |
double precision |
NO | — | Compliance/regulatory weight. |
model_version |
text |
NO | 'v1' |
Scoring model version for reproducibility. |
TimescaleDB Configuration:
| Setting | Value |
|---|---|
| Hypertable dimension | scored_at |
| Chunk interval | 7 days |
| Compression | Enabled after 30 days |
| Retention | 1 year (drop_chunks policy) |
Indexes:
| Name | Columns | Type |
|---|---|---|
ix_scores_grant_time |
(grant_id, scored_at DESC) |
B-tree |
ix_scores_scored_at |
scored_at DESC |
B-tree (chunk-skipping) |
access_scores_latest — Continuous Aggregate¶
A TimescaleDB continuous aggregate that materialises the latest score for every grant. Automatically refreshed.
| Column | Type | Description |
|---|---|---|
grant_id |
uuid |
The access grant. |
bucket |
timestamptz |
Time bucket (1-day granularity). |
avg_decay_score |
double precision |
Average score in the bucket. |
max_decay_score |
double precision |
Maximum score in the bucket. |
last_decay_score |
double precision |
Most recent score in the bucket. |
Continuous Aggregate Policy:
| Setting | Value |
|---|---|
| Bucket width | 1 day |
| Refresh start offset | 3 days |
| Refresh end offset | 1 hour |
| Schedule interval | 1 hour |
access_events — TimescaleDB Hypertable¶
Time-series table of all access activity events. Configured with 1-day chunks and a 7-year retention policy.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
grant_id |
uuid |
YES | — | FK → access_grants.id (nullable for pre-grant events). |
principal_id |
uuid |
NO | — | FK → principals.id. |
asset_id |
uuid |
NO | — | FK → assets.id. |
platform |
text |
NO | — | Source platform. |
event_type |
text |
NO | — | Category: login, query, api_call, permission_change. |
action |
text |
NO | — | Specific action performed. |
source_ip |
text |
YES | — | Client IP address. |
event_metadata |
jsonb |
YES | '{}' |
Platform-specific event details. |
occurred_at |
timestamptz |
NO | — | When the event occurred (hypertable partition key). |
ingested_at |
timestamptz |
NO | now() |
When Verity ingested the event. |
TimescaleDB Configuration:
| Setting | Value |
|---|---|
| Hypertable dimension | occurred_at |
| Chunk interval | 1 day |
| Compression | Enabled after 7 days |
| Retention | 7 years (drop_chunks policy) |
Indexes:
| Name | Columns | Type |
|---|---|---|
ix_events_grant_time |
(grant_id, occurred_at DESC) |
B-tree |
ix_events_principal_time |
(principal_id, occurred_at DESC) |
B-tree |
ix_events_platform |
platform |
B-tree |
ix_events_occurred_at |
occurred_at DESC |
B-tree (chunk-skipping) |
access_stats_latest¶
Pre-computed usage statistics for each grant, refreshed by the decay engine.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
grant_id |
uuid |
NO | — | FK → access_grants.id (PK). |
total_events |
bigint |
NO | 0 |
Lifetime event count. |
events_last_30d |
bigint |
NO | 0 |
Events in the last 30 days. |
events_last_90d |
bigint |
NO | 0 |
Events in the last 90 days. |
last_event_at |
timestamptz |
YES | — | Timestamp of the most recent event. |
computed_at |
timestamptz |
NO | now() |
When stats were last refreshed. |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_access_stats_latest |
grant_id |
PRIMARY KEY |
review_packets¶
Access-review work items generated when scores exceed thresholds.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
grant_id |
uuid |
NO | — | FK → access_grants.id. |
principal_id |
uuid |
NO | — | FK → principals.id. |
asset_id |
uuid |
NO | — | FK → assets.id. |
status |
text |
NO | 'created' |
Workflow state (see Data Flow). |
trigger_score |
double precision |
NO | — | Decay score that triggered the review. |
trigger_reason |
text |
YES | — | Human-readable trigger explanation. |
assigned_to |
uuid |
YES | — | FK → principals.id (the reviewer). |
workflow_run_id |
text |
YES | — | Temporal workflow run ID. |
created_at |
timestamptz |
NO | now() |
When the packet was created. |
updated_at |
timestamptz |
NO | now() |
Last status change. |
due_at |
timestamptz |
YES | — | SLA deadline for decision. |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_review_packets |
id |
PRIMARY KEY |
ix_reviews_grant |
grant_id |
B-tree |
ix_reviews_status |
status |
B-tree |
ix_reviews_assigned |
assigned_to |
B-tree |
ix_reviews_due_at |
due_at |
B-tree |
review_decisions¶
Immutable record of every review decision made.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
review_id |
uuid |
NO | — | FK → review_packets.id. |
decision |
text |
NO | — | One of: revoke, downgrade, maintain. |
justification |
text |
YES | — | Free-text justification from the reviewer. |
decided_by |
uuid |
NO | — | FK → principals.id. |
decided_by_email |
text |
YES | — | Email of the reviewer (denormalised for audit). |
decided_at |
timestamptz |
NO | now() |
When the decision was submitted. |
metadata |
jsonb |
YES | '{}' |
Additional context (e.g. auto-decision policy). |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_review_decisions |
id |
PRIMARY KEY |
ix_decisions_review |
review_id |
B-tree |
ix_decisions_decided_by |
decided_by |
B-tree |
ix_decisions_decided_at |
decided_at DESC |
B-tree |
connector_checkpoints¶
Tracks sync progress for each connector to enable incremental pulls.
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NO | gen_random_uuid() |
Primary key. |
connector_name |
text |
NO | — | Service name (e.g. connector-azure-ad). |
platform |
text |
NO | — | Platform identifier. |
checkpoint_type |
text |
NO | — | Type of checkpoint (e.g. delta_link, timestamp, page_token). |
checkpoint_value |
text |
NO | — | Opaque checkpoint value. |
metadata |
jsonb |
YES | '{}' |
Additional checkpoint context. |
created_at |
timestamptz |
NO | now() |
Row creation timestamp. |
updated_at |
timestamptz |
NO | now() |
Last checkpoint update. |
Indexes:
| Name | Columns | Type |
|---|---|---|
pk_connector_checkpoints |
id |
PRIMARY KEY |
uq_checkpoints_natural |
(connector_name, platform, checkpoint_type) |
UNIQUE |
ClickHouse — Audit Schema¶
verity_audit¶
Immutable, append-only audit log for all platform activity. Designed for high-throughput writes and fast analytical queries over long time ranges.
Engine: MergeTree
Partition By: toYYYYMM(occurred_at) (monthly partitions)
Order By: (entity_type, entity_id, occurred_at)
TTL: occurred_at + INTERVAL 7 YEAR DELETE (7-year retention)
| # | Column | Type | Description |
|---|---|---|---|
| 1 | event_id |
UUID |
Unique event identifier (globally unique). |
| 2 | occurred_at |
DateTime64(3) |
Millisecond-precision event timestamp. |
| 3 | actor_id |
String |
ID of the user or service that performed the action. |
| 4 | actor_email |
LowCardinality(Nullable(String)) |
Email address of the actor (if applicable). |
| 5 | action |
LowCardinality(String) |
Action performed (e.g. score.computed, review.decided, grant.revoked). |
| 6 | entity_type |
LowCardinality(String) |
Type of entity affected (e.g. grant, principal, review). |
| 7 | entity_id |
String |
ID of the affected entity. |
| 8 | entity_name |
LowCardinality(Nullable(String)) |
Human-readable name of the entity. |
| 9 | decision |
LowCardinality(Nullable(String)) |
Review decision if applicable (revoke, downgrade, maintain). |
| 10 | justification |
Nullable(String) |
Free-text justification for the action. |
| 11 | risk_level |
LowCardinality(Nullable(String)) |
Risk level at time of action (low, medium, high, critical). |
| 12 | source_ip |
Nullable(String) |
Source IP address of the actor. |
| 13 | metadata |
String |
JSON-encoded additional context. |
| 14 | regulation |
LowCardinality(Nullable(String)) |
Applicable regulation (e.g. SOX, GDPR, HIPAA, SOC2). |
| 15 | compliance_status |
LowCardinality(Nullable(String)) |
Compliance evaluation result (compliant, non_compliant, pending). |
| 16 | data_classification |
LowCardinality(Nullable(String)) |
Data classification of the affected asset (public, internal, confidential, restricted). |
| 17 | retention_years |
UInt8 |
Retention period in years (default: 7). |
ClickHouse DDL¶
CREATE TABLE verity_audit
(
event_id UUID,
occurred_at DateTime64(3),
actor_id String,
actor_email LowCardinality(Nullable(String)),
action LowCardinality(String),
entity_type LowCardinality(String),
entity_id String,
entity_name LowCardinality(Nullable(String)),
decision LowCardinality(Nullable(String)),
justification Nullable(String),
risk_level LowCardinality(Nullable(String)),
source_ip Nullable(String),
metadata String DEFAULT '{}',
regulation LowCardinality(Nullable(String)),
compliance_status LowCardinality(Nullable(String)),
data_classification LowCardinality(Nullable(String)),
retention_years UInt8 DEFAULT 7
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (entity_type, entity_id, occurred_at)
TTL occurred_at + INTERVAL 7 YEAR DELETE
SETTINGS index_granularity = 8192;
ClickHouse Design Notes¶
| Aspect | Details |
|---|---|
| Write pattern | High-throughput batch inserts from audit-writer via the ClickHouse HTTP interface. |
| Read pattern | Analytical queries — compliance reports, audit trails, entity history. |
| LowCardinality | Applied to enum-like columns (action, entity_type, risk_level, etc.) for dictionary encoding and compression. |
| Partitioning | Monthly partitions (toYYYYMM) balance partition count with query pruning. |
| Ordering | (entity_type, entity_id, occurred_at) optimises the most common query: "show me all events for entity X". |
| TTL | 7-year retention enforced at the engine level; ClickHouse automatically drops expired partitions. |
| Immutability | The audit-writer service only performs INSERT operations. No UPDATE or DELETE queries are permitted. |