Database Schema

Triage Warden supports both SQLite (development/small deployments) and PostgreSQL (production). This document describes the database schema used by both backends.

Overview

The database consists of 13 tables organized into four logical groups:

  • Core Incident Management: incidents, audit_logs, actions, approvals
  • Configuration: playbooks, connectors, policies, notification_channels, settings
  • Authentication: users, sessions, api_keys
  • Multi-Tenancy: tenants, feature_flags

Multi-Tenancy

All tenant-scoped tables include a tenant_id foreign key that references the tenants table. In PostgreSQL, Row-Level Security (RLS) policies automatically filter all queries by the current tenant context.

tenants

Tenant organizations in a multi-tenant deployment.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
nameTEXTNOT NULLOrganization display name
slugTEXTUNIQUE, NOT NULLURL-safe identifier for routing
statusENUM/TEXTDEFAULT 'active'active, suspended, pending_deletion
settingsJSON/TEXTDEFAULT '{}'Tenant-specific settings
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Indexes: slug (unique), status

feature_flags

Feature flag configuration for gradual rollouts.

ColumnTypeConstraintsDescription
nameTEXTPRIMARY KEYFlag name
descriptionTEXTDEFAULT ''Flag description
default_enabledBOOLEANDEFAULT FALSEDefault state
tenant_overridesJSONDEFAULT '{}'Per-tenant overrides
percentage_rolloutINTEGERNULLABLE0-100 percentage rollout
created_atTIMESTAMPNOT NULLCreation timestamp
updated_atTIMESTAMPNOT NULLLast update timestamp

Note: The tenants and feature_flags tables are NOT protected by RLS.

Entity Relationship Diagram

┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│    users     │       │   api_keys   │       │   sessions   │
├──────────────┤       ├──────────────┤       ├──────────────┤
│ id (PK)      │◄──────│ user_id (FK) │       │ id (PK)      │
│ email        │       │ id (PK)      │       │ data         │
│ username     │       │ key_hash     │       │ expiry_date  │
│ password_hash│       │ scopes       │       └──────────────┘
│ role         │       └──────────────┘
└──────────────┘

┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│  incidents   │       │  audit_logs  │       │   actions    │
├──────────────┤       ├──────────────┤       ├──────────────┤
│ id (PK)      │◄──────│ incident_id  │       │ id (PK)      │
│ source       │       │ id (PK)      │       │ incident_id  │──┐
│ severity     │       │ action       │       │ action_type  │  │
│ status       │◄──────│ actor        │       │ target       │  │
│ alert_data   │       │ details      │       │ approval_status│ │
│ enrichments  │       │ created_at   │       └──────────────┘  │
│ analysis     │       └──────────────┘                         │
│ proposed_actions│                                             │
│ ticket_id    │       ┌──────────────┐                         │
│ tags         │       │  approvals   │◄────────────────────────┘
│ metadata     │       ├──────────────┤
└──────────────┘       │ id (PK)      │
                       │ action_id    │
                       │ incident_id  │
                       │ status       │
                       └──────────────┘

Core Tables

incidents

Stores security incidents created from incoming alerts.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
tenant_idUUID/TEXTFK → tenants, NOT NULLOwning tenant
sourceJSON/TEXTNOT NULLAlert source metadata
severityENUM/TEXTNOT NULLinfo, low, medium, high, critical
statusENUM/TEXTNOT NULLSee Status Values
alert_dataJSON/TEXTNOT NULLOriginal alert payload
enrichmentsJSON/TEXTDEFAULT '[]'Array of enrichment results
analysisJSON/TEXTNULLABLEAI triage analysis
proposed_actionsJSON/TEXTDEFAULT '[]'Array of proposed actions
ticket_idTEXTNULLABLEExternal ticket reference
tagsJSON/TEXTDEFAULT '[]'User-defined tags
metadataJSON/TEXTDEFAULT '{}'Additional metadata
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Indexes: (tenant_id, status), (tenant_id, severity), (tenant_id, created_at), status, severity, created_at, updated_at

RLS: Protected by Row-Level Security in PostgreSQL.

Incident Status Values

  • new - Newly created from alert
  • enriching - Gathering threat intelligence
  • analyzing - AI analysis in progress
  • pending_review - Awaiting analyst review
  • pending_approval - Actions awaiting approval
  • executing - Actions being executed
  • resolved - Incident resolved
  • false_positive - Marked as false positive
  • escalated - Escalated to higher tier
  • closed - Administratively closed

audit_logs

Immutable audit trail for all incident actions.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
incident_idUUID/TEXTFK → incidentsParent incident
actionTEXTNOT NULLAction type (status_changed, action_approved, etc.)
actorTEXTNOT NULLUsername or "system"
detailsJSON/TEXTNULLABLEAction-specific details
created_atTIMESTAMP/TEXTNOT NULLAction timestamp

Indexes: incident_id, created_at

actions

Stores proposed and executed response actions.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
incident_idUUID/TEXTFK → incidentsParent incident
action_typeTEXTNOT NULLisolate_host, disable_user, block_ip, etc.
targetJSON/TEXTNOT NULLAction target details
parametersJSON/TEXTDEFAULT '{}'Action parameters
reasonTEXTNOT NULLJustification for action
priorityINTEGERDEFAULT 50Execution priority (1-100)
approval_statusENUM/TEXTNOT NULLSee Approval Status Values
approved_byTEXTNULLABLEApproving user
approval_timestampTIMESTAMP/TEXTNULLABLEApproval time
resultJSON/TEXTNULLABLEExecution result
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
executed_atTIMESTAMP/TEXTNULLABLEExecution timestamp

Indexes: incident_id, approval_status, created_at

Approval Status Values

  • pending - Awaiting approval decision
  • auto_approved - Automatically approved by policy
  • approved - Manually approved
  • denied - Manually denied
  • executed - Successfully executed
  • failed - Execution failed

approvals

Tracks multi-level approval workflows.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
action_idUUID/TEXTFK → actionsRelated action
incident_idUUID/TEXTFK → incidentsParent incident
approval_levelTEXTNOT NULLanalyst, senior, manager, executive
statusENUM/TEXTNOT NULLpending, approved, denied, expired
requested_byTEXTNOT NULLRequesting user/system
requested_atTIMESTAMP/TEXTNOT NULLRequest timestamp
decided_byTEXTNULLABLEDeciding user
decided_atTIMESTAMP/TEXTNULLABLEDecision timestamp
decision_reasonTEXTNULLABLEOptional reason
expires_atTIMESTAMP/TEXTNULLABLEApproval expiration

Indexes: action_id, status, expires_at

Configuration Tables

playbooks

Automation workflow definitions.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
nameTEXTNOT NULLPlaybook name
descriptionTEXTNULLABLEDescription
trigger_typeTEXTNOT NULLalert_type, severity, source, manual
trigger_conditionTEXTNULLABLETrigger condition expression
stagesJSON/TEXTDEFAULT '[]'Array of workflow stages
enabledBOOLEAN/INTEGERDEFAULT TRUEActive status
execution_countINTEGERDEFAULT 0Times executed
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Indexes: name, trigger_type, enabled, created_at

connectors

External integration configurations.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
nameTEXTNOT NULLDisplay name
connector_typeTEXTNOT NULLvirus_total, jira, splunk, etc.
configJSON/TEXTDEFAULT '{}'Connection configuration (encrypted credentials)
statusTEXTDEFAULT 'unknown'connected, disconnected, error, unknown
enabledBOOLEAN/INTEGERDEFAULT TRUEActive status
last_health_checkTIMESTAMP/TEXTNULLABLELast health check time
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Indexes: name, connector_type, status, enabled

policies

Approval and automation policy rules.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
nameTEXTNOT NULLPolicy name
descriptionTEXTNULLABLEDescription
conditionTEXTNOT NULLCondition expression
actionTEXTNOT NULLauto_approve, require_approval, deny
approval_levelTEXTNULLABLERequired approval level
priorityINTEGERDEFAULT 0Evaluation priority
enabledBOOLEAN/INTEGERDEFAULT TRUEActive status
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Indexes: name, action, priority, enabled

notification_channels

Alert notification configurations.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
nameTEXTNOT NULLChannel name
channel_typeTEXTNOT NULLslack, teams, email, pagerduty, webhook
configJSON/TEXTDEFAULT '{}'Channel configuration
eventsJSON/TEXTDEFAULT '[]'Subscribed event types
enabledBOOLEAN/INTEGERDEFAULT TRUEActive status
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Indexes: name, channel_type, enabled

settings

Key-value configuration store.

ColumnTypeConstraintsDescription
keyTEXTPRIMARY KEYSetting key (general, rate_limits, llm)
valueJSON/TEXTNOT NULLSetting value as JSON
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Authentication Tables

users

User accounts for dashboard and API access.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
emailTEXTUNIQUE, NOT NULLEmail address
usernameTEXTUNIQUE, NOT NULLLogin username
password_hashTEXTNOT NULLArgon2 password hash
roleENUM/TEXTNOT NULLadmin, analyst, viewer
display_nameTEXTNULLABLEDisplay name
enabledBOOLEAN/INTEGERDEFAULT TRUEAccount active status
last_login_atTIMESTAMP/TEXTNULLABLELast login timestamp
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp
updated_atTIMESTAMP/TEXTNOT NULLLast update timestamp

Indexes: email, username, role, enabled

sessions

User session storage (tower-sessions compatible).

ColumnTypeConstraintsDescription
idTEXTPRIMARY KEYSession ID
dataBLOBNOT NULLEncrypted session data
expiry_dateINTEGERNOT NULLUnix timestamp expiration

Indexes: expiry_date

api_keys

API key authentication.

ColumnTypeConstraintsDescription
idUUID/TEXTPRIMARY KEYUnique identifier
user_idUUID/TEXTFK → usersOwner user
nameTEXTNOT NULLKey display name
key_hashTEXTNOT NULLSHA-256 hash of key
key_prefixTEXTNOT NULLFirst 8 chars for identification
scopesJSON/TEXTDEFAULT '[]'Allowed API scopes
expires_atTIMESTAMP/TEXTNULLABLEKey expiration
last_used_atTIMESTAMP/TEXTNULLABLELast usage timestamp
created_atTIMESTAMP/TEXTNOT NULLCreation timestamp

Indexes: user_id, key_prefix, expires_at

Database-Specific Notes

SQLite

  • UUIDs stored as TEXT
  • Timestamps stored as ISO 8601 TEXT
  • Boolean stored as INTEGER (0/1)
  • JSON stored as TEXT
  • Uses CHECK constraints for enums

PostgreSQL

  • Native UUID type
  • Native TIMESTAMPTZ type
  • Native BOOLEAN type
  • Native JSONB type with indexing
  • Uses custom ENUM types for status fields
  • Row-Level Security (RLS) enabled on all tenant-scoped tables

Row-Level Security

PostgreSQL deployments use RLS for defense-in-depth tenant isolation:

-- RLS policy example (automatically applied to all queries)
CREATE POLICY incidents_select_tenant_isolation ON incidents
    FOR SELECT
    USING (tenant_id = current_setting('app.current_tenant', true)::uuid);

To set the tenant context:

-- Set before executing tenant-scoped queries
SELECT set_tenant_context('00000000-0000-0000-0000-000000000001'::uuid);

-- Or use the session variable directly
SET app.current_tenant = '00000000-0000-0000-0000-000000000001';

Helper functions:

FunctionDescription
set_tenant_context(uuid)Sets tenant context, returns previous value
get_current_tenant()Returns current tenant UUID or NULL
clear_tenant_context()Clears tenant context

Migrations

Migrations are managed by SQLx and located in:

  • SQLite: crates/tw-core/src/db/migrations/sqlite/
  • PostgreSQL: crates/tw-core/src/db/migrations/postgres/

Run migrations automatically on startup or manually:

# SQLite
tw-cli db migrate --database-url "sqlite:data/triage.db"

# PostgreSQL
tw-cli db migrate --database-url "postgres://user:pass@host/db"