Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Database Schema

siggy uses SQLite with WAL (Write-Ahead Logging) mode for safe concurrent reads/writes. The database file is stored alongside the config file.

Tables

schema_version

Tracks the current migration version.

CREATE TABLE schema_version (
    version INTEGER NOT NULL
);

conversations

One row per conversation (1:1 or group).

CREATE TABLE conversations (
    id                TEXT PRIMARY KEY,      -- phone number or group ID
    name              TEXT NOT NULL,         -- display name
    is_group          INTEGER NOT NULL DEFAULT 0,
    created_at        TEXT NOT NULL DEFAULT (datetime('now')),
    muted             INTEGER NOT NULL DEFAULT 0,  -- added in migration v2
    expiration_timer  INTEGER NOT NULL DEFAULT 0,  -- disappearing msg seconds (v7)
    accepted          INTEGER NOT NULL DEFAULT 1,  -- message request state (v8)
    blocked           INTEGER NOT NULL DEFAULT 0   -- blocked state (v9)
);

The id is a phone number (E.164 format) for 1:1 conversations or a base64-encoded group ID for groups.

messages

All messages, ordered by insertion rowid.

CREATE TABLE messages (
    rowid           INTEGER PRIMARY KEY AUTOINCREMENT,
    conversation_id TEXT NOT NULL REFERENCES conversations(id),
    sender          TEXT NOT NULL,       -- sender display name or empty for system
    timestamp       TEXT NOT NULL,       -- RFC 3339 timestamp
    body            TEXT NOT NULL,       -- message text
    is_system       INTEGER NOT NULL DEFAULT 0,
    status          INTEGER NOT NULL DEFAULT 0,    -- MessageStatus enum (v3)
    timestamp_ms    INTEGER NOT NULL DEFAULT 0,    -- server epoch ms (v3)
    is_edited       INTEGER NOT NULL DEFAULT 0,    -- edited flag (v6)
    is_deleted      INTEGER NOT NULL DEFAULT 0,    -- deleted flag (v6)
    quote_author    TEXT,                           -- quoted reply author (v6)
    quote_body      TEXT,                           -- quoted reply body (v6)
    quote_ts_ms     INTEGER,                        -- quoted reply timestamp (v6)
    sender_id            TEXT NOT NULL DEFAULT '',       -- sender phone number (v6)
    expires_in_seconds   INTEGER NOT NULL DEFAULT 0,    -- disappearing timer (v7)
    expiration_start_ms  INTEGER NOT NULL DEFAULT 0     -- timer start epoch ms (v7)
);

CREATE INDEX idx_messages_conv_ts ON messages(conversation_id, timestamp);
CREATE INDEX idx_messages_conv_ts_ms ON messages(conversation_id, timestamp_ms);

System messages (is_system = 1) are used for join/leave notifications and are excluded from unread counts.

reactions

Emoji reactions on messages. One reaction per sender per message, with the latest emoji replacing any previous one.

CREATE TABLE reactions (
    rowid           INTEGER PRIMARY KEY AUTOINCREMENT,
    conversation_id TEXT NOT NULL,
    target_ts_ms    INTEGER NOT NULL,     -- timestamp of the reacted-to message
    target_author   TEXT NOT NULL,         -- author of the reacted-to message
    emoji           TEXT NOT NULL,
    sender          TEXT NOT NULL,         -- who sent this reaction
    UNIQUE(conversation_id, target_ts_ms, target_author, sender)
);

CREATE INDEX idx_reactions_target ON reactions(conversation_id, target_ts_ms);

read_markers

Tracks the last-read message per conversation for unread counting.

CREATE TABLE read_markers (
    conversation_id TEXT PRIMARY KEY REFERENCES conversations(id),
    last_read_rowid INTEGER NOT NULL DEFAULT 0
);

Unread count = messages with rowid > last_read_rowid and is_system = 0.

Migrations

Migrations are version-based and run sequentially in Database::migrate():

VersionChanges
1Initial schema: conversations, messages, read_markers tables
2Add muted column to conversations
3Add status and timestamp_ms columns to messages (delivery status tracking)
4Create reactions table with unique constraint per sender per message
5Add index on messages(conversation_id, timestamp_ms) for search performance
6Add is_edited, is_deleted, quote_author, quote_body, quote_ts_ms, sender_id columns to messages
7Add expiration_timer to conversations and expires_in_seconds, expiration_start_ms to messages
8Add accepted column to conversations (message request tracking)
9Add blocked column to conversations (block/unblock state)

Each migration is wrapped in a transaction. The schema_version table tracks the current version.

WAL mode

WAL mode is enabled on every connection:

PRAGMA journal_mode=WAL;
PRAGMA foreign_keys=ON;

WAL allows concurrent readers while a writer is active, preventing database locks during normal operation.

In-memory mode

When running with --incognito, Database::open_in_memory() is used instead of Database::open(). The same schema and migrations apply, but everything lives in memory and is lost on exit.