Database Schema

SQLite configuration, table summary with indexes, the Postgres migration rules, and the audit append-only constraint.

SYRIS uses SQLite in WAL mode as the default database. The schema is designed to be Postgres-compatible with three trivial changes.

SQLite configuration

The following PRAGMAs must be applied on every connection:

PRAGMA journal_mode = WAL;       -- Write-Ahead Logging: concurrent reads + one writer
PRAGMA synchronous  = NORMAL;    -- Safe with WAL; avoids fsync on every commit
PRAGMA foreign_keys = ON;        -- Enforce referential integrity
PRAGMA busy_timeout = 5000;      -- ms; prevents "database is locked" under load

WAL is required because the API server and background loops read the DB concurrently while the pipeline writes it. synchronous = NORMAL is safe with WAL because WAL itself provides crash safety. busy_timeout prevents lock errors during brief write contention.

Table summary

TablePurposeKey indexes
eventsInbound events after normalisationtrace_id, ingested_at, channel, dedupe_key (unique partial)
routing_decisionsRouting outcome per event chain (1:1 with trace)event_id, decided_at
tasksPersisted workflow instancesstatus, trace_id, next_wake_time (partial), updated_at
stepsIndividual resumable units within taskstask_id, status, idempotency_key (partial)
tool_callsRecord of every tool invocationtrace_id, idempotency_key, created_at
tool_resultsFinal outcome per tool call (FK to tool_calls)PK = tool_call_id
idempotency_outcomesStable outcome store for dedup on retry/restartPK = idempotency_key
audit_eventsAppend-only operational history — never updated or deletedtrace_id, timestamp, type, outcome, tool_name, ref_task_id (partial)
approvalsPending and resolved gate decisionsstatus, trace_id, expires_at
schedulesPersistent schedule definitionsnext_run_at (partial, enabled = 1 only)
watcher_statePer-watcher tick, error, and dedup statePK = watcher_id
rulesIFTTT-style condition → action rulesenabled, priority
system_healthPeriodic heartbeat records from HeartbeatWatcherrecorded_at
alarmsOpen and resolved incidentsdedupe_key (unique partial, open only), status
artifactsEncrypted, redacted audit payloads referenced by payload_refref_id, created_at

Field-level schemas for events, audit_events, and approvals are in architecture/data-contracts. Full DDL lives in storage/migrations/.

Append-only constraint on audit_events

audit_events must never be updated or deleted by application code. This is enforced by convention at the application layer: AuditWriter is the only code that writes to this table, and it only issues INSERT.

At the DB level, no cascade deletes reference audit_events. If enforcement at the DB level is required, a trigger that raises on UPDATE or DELETE of audit_events can be added as a migration step.

Postgres migration rules

The SQLite schema above is valid Postgres SQL with exactly three changes:

  1. INTEGER PRIMARY KEY AUTOINCREMENTSERIAL PRIMARY KEY
  2. INTEGER boolean columns (0/1) → BOOLEAN
  3. Partial index WHERE syntax is identical in Postgres — no change required

Use Alembic with render_as_batch = True during SQLite development for ALTER TABLE compatibility. When migrating to Postgres, use alembic --sql to generate reviewed DDL rather than migrating a live database directly.