arizuko

arizukoreference › SQLite schema

schema

Every SQLite table in arizuko. Tables now live across the split daemons' own DBs — routd.db, runed.db, auth.db, onbod.db — and each daemon owns and migrates its own tables. The shared schema library lives in store/migrations/ (embedded via store/store.go:13); no single daemon applies every migration. State described here is the cumulative result of migrations 0001 through 0071 — the 0049–0057 run added cost-log, proxyd-routes, unified ACL, topic lineage, pane sessions, and group visibility; 0058–0071 added the engagement column, route tokens, the group model + container config, the CLI / IPC / config audit tables, group watchers, platform IDs, the 0065 orphan-table drops, the unified audit_log, config_meta, FK constraints on web routes and route tokens, full-text search over messages, and the onboarding admitted_at column.

35 live tables, grouped by concern, plus the messages_fts virtual table.

1. Identity & ACL

auth_users

OAuth-issued user identities. One row per subject across all linked providers; linked_to_sub points at the canonical row when this row is a secondary identity.

columntypenulldefaultmeaning
idINTEGERPK autoincrement
subTEXTNOT NULL UNIQUEOAuth subject (e.g. github:1234, google:abc)
usernameTEXTNOT NULL UNIQUEdisplay handle
hashTEXTNOT NULLpassword hash (legacy local-auth)
nameTEXTNOT NULLfull name
created_atTEXTNOT NULLRFC3339
linked_to_subTEXTNULLcanonical sub when this row is a linked identity (0040)

Indexes: PK on id; unique on sub, username; partial idx_auth_users_linked_to_sub on linked_to_sub WHERE linked_to_sub IS NOT NULL.

Writers: store/auth.go:28 (INSERT), :90 (UPDATE linked_to_sub), :95 (INSERT linked), onbod/main.go:468 (UPDATE username on claim).

Readers: proxyd (login), webd, dashd, onbod.

Migrations: created 0001; 0040 added linked_to_sub.

auth_sessions

Server-side session table (refresh-token store). JWTs are short-lived; this row carries the long-lived refresh state.

columntypenullmeaning
token_hashTEXT PKNOT NULLSHA-256 of the cookie token
user_subTEXTNOT NULLowning auth_users.sub
expires_atTEXTNOT NULLRFC3339
created_atTEXTNOT NULLRFC3339

Writers: store/auth.go:121 (INSERT), :144 (DELETE).

Readers: proxyd, webd.

Migrations: created 0001.

acl

Unified permission rows. Answers "may principal P perform action A on scope S?" Replaced the legacy user_groups + grant_rules tables in v0.38.0. Principals are OAuth subs (google:…), folder agents (folder:…), room/user JIDs, or roles (role:operator). Match logic in auth.Authorize; effect='deny' wins over allow.

columntypenulldefaultmeaning
principalTEXTNOT NULLsub, folder, JID, or role:<name>
actionTEXTNOT NULLinteract, admin, mcp:<tool>, or *
scopeTEXTNOT NULLfolder glob
effectTEXTNOT NULL'allow''allow' or 'deny'
paramsTEXTNOT NULL''JSON predicate constraints (e.g. {"jid":"telegram:*"})
predicateTEXTNOT NULL''reserved for richer expressions
granted_byTEXTNULLactor source (CLI / dashd / migration)
granted_atTEXTNOT NULLRFC3339

PK: (principal, action, scope, params, predicate, effect). Indexes: acl_by_principal_action, acl_by_scope.

Writers: store/acl.go (AddACLRow, RemoveACLRow); cmd/arizuko grant; dashd ACL editor; migration 0053 (back-fill).

Readers: many — proxyd, webd, dashd, onbod, routd (every folder-scoped HTTP route via auth.Authorize); MCP list_acl.

Migrations: created 0052; 0053 back-filled from user_groups + grant_rules then dropped those tables.

acl_membership

Identity and role indirection. One edge (child, parent) means "anything child proves it is, it also is parent." Used for role membership ((sub, role:operator)) and for OAuth-mediated JID claims ((discord:user/811…, google:114alice)). Walked transitively at authorization time.

columntypenullmeaning
childTEXTNOT NULLthe holder of the identity
parentTEXTNOT NULLthe role or canonical identity it expands to
added_byTEXTNULLactor source
added_atTEXTNOT NULLRFC3339

PK: (child, parent). Indexes: acl_membership_by_child.

Writers: store/acl.go (AddMembership, RemoveMembership); arizuko grant <sub> **; OAuth callback for JID claims; migration 0053 (back-fill of pre-0053 user_groups('**') rows and user_jids bindings).

Readers: routd, proxyd, webd, dashd, onbod (via auth.Authorize walking Ancestors(sub)).

Migrations: created 0052.

identities + identity_claims + identity_codes

Cross-channel identity merge (spec 5/9). One identities row is a canonical person; identity_claims binds one or more platform sender subs to it. Subs are bound via the authd OAuth intent=link flow. Advisory — agents read via inspect_identity, never enforce. (identity_codes survives as a migration-history table; the chat-paste link-code flow that wrote it was retired.)

table.columntypemeaning
identities.idTEXT PKcanonical identity ID
identities.nameTEXT NOT NULLdisplay name
identities.created_atTEXT NOT NULLRFC3339
identity_claims.subTEXT PKplatform sender sub
identity_claims.identity_idTEXT NOT NULLidentities.id
identity_claims.claimed_atTEXT NOT NULLRFC3339
identity_codes.codeTEXT PKshort-lived link code
identity_codes.identity_idTEXT NOT NULLidentities.id
identity_codes.expires_atTEXT NOT NULLRFC3339

Indexes: idx_identity_claims_id on identity_claims(identity_id).

Writers: store/identities.goCreateIdentity (INSERT identity), LinkSub (INSERT OR REPLACE claim), UnlinkSub (DELETE claim). identity_codes has no live writer.

Readers: authd (inspect_identity MCP tool; OAuth intent=link binds subs).

Migrations: all three created 0035.

2. Messages, sessions, turns

messages

The main bus. Every inbound and outbound message lands here. Rebuilt by migration 0023 to drop dead columns and re-source identity around source.

columntypenulldefaultmeaning
idTEXT PKNOT NULLplatform-specific message ID
chat_jidTEXTNOT NULLtyped JID of the chat
senderTEXTNOT NULLtyped sender JID
sender_nameTEXTNULLdisplay name
contentTEXTNOT NULLmessage body
timestampTEXTNOT NULLRFC3339Nano
is_from_meINTEGERNOT NULL0set on outbound
is_bot_messageINTEGERNOT NULL0set on agent output
forwarded_fromTEXTNULLupstream forward source
reply_to_idTEXTNULL0003
reply_to_textTEXTNULLparent excerpt
reply_to_senderTEXTNULLparent sender (typed JID after 0042)
topicTEXTNOT NULL''topic-sessions key (0008)
routed_toTEXTNOT NULL''folder this message was routed to (0015)
verbTEXTNOT NULL'message'event type (0017)
attachmentsTEXTNOT NULL''JSON []chanlib.InboundAttachment (0019)
sourceTEXTNOT NULL''handling adapter; receiver inbound, deliverer outbound
erroredINTEGERNOT NULL0marked when a prior run failed; re-fed tagged (0030)
turn_idTEXTNULLinbound msg ID that triggered this outbound (0038)
statusTEXTNOT NULL'sent''sent' | 'pending' | 'failed' (0039)
chat_nameTEXTNOT NULL''human channel/group name (0046)
link_contextTEXTNULLroute token's context snapshotted at ingest; rendered as <link-context> in the agent prompt

Indexes: idx_messages_chat_ts on (chat_jid, timestamp); idx_messages_turn_id on turn_id WHERE turn_id IS NOT NULL; idx_messages_status on status WHERE status != 'sent'.

Writers: store/messages.go:19 (PutMessage), timed/main.go:132 (scheduled-task inject); UPDATEs in store/messages.go (status, content, errored, delivered).

Readers: many — routd, dashd, webd, davd, vited, router, ipc.

Migrations: 0001, 0003 (reply_to_id), 0005 (source, group_folder), 0008 (topic), 0015 (routed_to), 0017 (verb), 0019 (attachments), 0023 (rebuild; drop group_folder), 0030 (errored), 0038 (turn_id), 0039 (status), 0042/0043 (typed JIDs), 0046 (chat_name).

messages_fts FTS5 shadow

An FTS5 virtual table over messages.content, kept in sync by triggers. Backs the find_messages MCP tool. messages.id is TEXT, so the shadow keys on the implicit INTEGER rowid SQLite gives the table. Tokenizer is unicode61 remove_diacritics 2 — Czech, Spanish, Japanese match without surprises (Czech "úroveň" matches "uroven").

columntypenulldefaultmeaning
contentFTS5indexed copy of messages.content
rowidINTEGERcontent_rowid='rowid' — joins back to messages.rowid

Sync: triggers messages_fts_ai (insert), messages_fts_au (update), messages_fts_ad (delete) mirror every change. Queried via messages_fts MATCH ? with bm25() rank and snippet() highlight.

Readers: store/messages.go:700 (FindMessages).

Migrations: 0070.

sessions

The active Claude Code session ID per (folder, topic). Used to resume an existing conversation in the in-container agent.

columntypenulldefaultmeaning
group_folderTEXTNOT NULLfolder path
topicTEXTNOT NULL''topic key
session_idTEXTNOT NULLClaude Code session UUID
parent_topicTEXTNULLtopic this one forked from ('' = main); NULL when no lineage (main itself)
forked_atTEXTNULLRFC3339Nano UTC fork point — audit metadata (the parent’s session file is already copied into the child’s session)
observed_cursorTEXTNULLper-topic high-water mark over is_observed=1 messages; gateway advances it after each turn

PK: (group_folder, topic).

Writers: store/sessions.goSetSession (UPSERT session_id), EnsureTopicLineage (seed lineage on first turn), ForkTopic (explicit branch), UpdateObservedCursor, DeleteSession.

Readers: routd.

Migrations: created 0001; rebuilt 0008 with topic column + composite PK; 0055 add parent_topic, forked_at, observed_cursor (spec 7/F).

session_log

Audit log of every agent run.

columntypemeaning
idINTEGER PK AUTOINCREMENT
group_folderTEXT NOT NULLfolder
session_idTEXT NOT NULLClaude Code session UUID
started_atTEXT NOT NULLRFC3339
ended_atTEXT NULLRFC3339
resultTEXT NULLoutcome summary
errorTEXT NULLerror message
message_countINTEGER NULLmessages processed

Writers: store/sessions.go:91 (gateway, at spawn end).

Readers: dashd.

Migrations: created 0001.

turn_results

Idempotent record per (folder, turn_id) so a webd round-handle protocol can fetch all frames of a single run by FK lookup instead of timestamp correlation.

columntypemeaning
folderTEXT NOT NULL
turn_idTEXT NOT NULLinbound message ID
session_idTEXT NULLresumed Claude session
statusTEXT NOT NULL'ok' / 'error'
recorded_atTEXT NOT NULLRFC3339Nano

PK: (folder, turn_id).

Writers: store/turns.go:15 (gateway).

Readers: webd (round-handle for /chat/<token>/{turn_id}/...).

Migrations: created 0036.

system_messages

System-origin events (e.g. migration broadcasts) merged into the agent's message stream.

columntypemeaning
idINTEGER PK AUTOINCREMENT
group_idTEXT NOT NULLtarget folder
originTEXT NOT NULL'self', 'platform', etc.
eventTEXT NOT NULLevent name
attrsTEXT NULLJSON attrs
bodyTEXT NOT NULL DEFAULT ''event payload
created_atTEXT NOT NULLRFC3339

Writers: store/sessions.go:54.

Readers: routd.

Migrations: created 0001.

chat_reply_state

Last bot reply ID per (chat_jid, topic) — used to chain assistant replies into a single visible thread on platforms that support thread/reply pointers.

columntypemeaning
jidTEXT NOT NULLchat JID (typed after 0043)
topicTEXT NOT NULL DEFAULT ''topic key
last_reply_idTEXT NOT NULLlast bot message ID

PK: (jid, topic).

Writers: store/messages.go:401.

Readers: gateway/adapter outbound layer.

Migrations: created 0014; 0043 rewrote rows to typed JID.

pane_sessions

Slack assistant-pane state per (team, user, thread_ts). Persists across slakd restarts; the in-memory map is a read-through cache. See slack pane for the concept.

columntypenullmeaning
team_idTEXTNOT NULLSlack workspace ID
user_idTEXTNOT NULLSlack user who opened the pane
thread_tsTEXTNOT NULLpane thread timestamp
channel_idTEXTNOT NULLDM channel that hosts the pane
context_jidTEXTNULLworkspace channel the user is currently viewing
opened_atTEXTNOT NULLRFC3339Nano UTC
last_status_atTEXTNULLlast setStatus call — used to debounce

PK: (team_id, user_id, thread_ts). Index: idx_pane_sessions_channel on channel_id.

Writers: store/pane_sessions.goUpsertPane, SetPaneContext, SetPaneStatusAt.

Readers: slakd (pane lookup keyed by channel), gateway (pane-context hint in buildAgentPrompt).

Migrations: created 0056.

3. Groups (folders), chats, routes

groups

The folder hierarchy. Path is identity (tier from depth via auth.Resolve). Created in 0020 by renaming the older registered_groups table and rekeying by folder.

columntypenulldefaultmeaning
folderTEXT PKNOT NULLfolder path (e.g. atlas/support)
nameTEXTNOT NULLdisplay name
added_atTEXTNOT NULLRFC3339
container_configTEXTNULLJSON GroupConfig (mounts, timeout, max_children)
parentTEXTNULLparent folder path
updated_atTEXTNULLRFC3339
productTEXTNOT NULL'assistant'product slug (0044)
openINTEGERNOT NULL11 = visible to sibling folders as ambient source (cross-folder <observed>); 0 = sealed off. Spec 7/F. Toggled via set_group_open.
observe_window_messagesINTEGERNULLper-group cap on ambient messages surfaced per turn (NULL = fall back to OBSERVE_WINDOW_MESSAGES). Per-route override wins.
observe_window_charsINTEGERNULLper-group cap on ambient chars per turn (NULL = fall back to OBSERVE_WINDOW_CHARS).

Writers: store/groups.go:30 (INSERT), :50 (DELETE), :250 (SetGroupOpen), SetGroupObserveWindow.

Readers: many — routd, dashd, webd, davd, onbod, timed, every MCP tool that scopes to a folder.

Migrations: created 0020; 0041 dropped state, spawn_ttl_days, archive_closed_days; 0044 added product; 0057 added open, observe_window_messages, observe_window_chars (spec 7/F).

chats

Per-chat state: agent cursor, sticky routing pins, single-user/group classification. Rebuilt 0023; is_group re-added 0033.

columntypenulldefaultmeaning
jidTEXT PKNOT NULLtyped chat JID
agent_cursorTEXTNULLlast-processed message ID (0020 moved from registered_groups)
sticky_groupTEXTNULLfolder pin via @folder command (0016)
sticky_topicTEXTNULLtopic pin via #topic command (0016)
is_groupINTEGERNOT NULL01 = group chat; gates user-secret overlay (0033)

Writers: store/groups.go:84 (upsert cursor), :209 (is_group), :237 (sticky_group), :252 (sticky_topic).

Readers: routd (routing, cursor, single-user check).

Migrations: 0001, 0002 (channel-prefix normalize), 0016 (sticky_group/sticky_topic), 0020 (added agent_cursor), 0023 (rebuild: dropped name/channel/is_group/last_message_time/errored), 0030 (DROP errored), 0033 (re-add is_group), 0042 (typed JIDs in PK).

routes

Inbound match-target table. match is space-separated key=glob pairs evaluated by router.RouteMatches; target is the destination folder with an optional #fragment. #observe is the only reserved mode (store without firing a turn); any other fragment (folder#deploy, folder#triage) pins the routed message to that topic. Routes evaluate in (seq, id) order; first match wins.

columntypenulldefaultmeaning
idINTEGER PK AUTOINCREMENTNOT NULL
seqINTEGERNOT NULL0evaluation order (lower first)
matchTEXTNOT NULL''space-separated key=glob predicates
targetTEXTNOT NULLdestination folder, optionally suffixed with #mode
observe_window_messagesINTEGERNULLper-route override for the trailing #observe message count folded into a trigger turn (0/NULL = fall back to OBSERVE_WINDOW_MESSAGES)
observe_window_charsINTEGERNULLper-route char cap on the #observe context window (0/NULL = fall back to OBSERVE_WINDOW_CHARS)

Indexes: idx_routes_seq on seq (post-0022 rebuild).

Writers: store/routes.go:38 (AddRoute), :55 (DELETE on folder tear-down), :62 (replace set), :72 (DELETE by id), onbod/main.go:974/:1039.

Readers: routd (ResolveRoute), dashd, ipc (route MCP tools).

Migrations: created 0001; 0008 ensure-exists + unique index; 0012 fix prefix seq; 0022 rebuild (drop jid/type, add match); 0042/0043 rewrite predicates to typed JIDs; 0054 drop impulse_config, encode mode in target fragment, add observe_window_*.

route_tokens

URL-bound credentials. One row per issued URL; the JID prefix drives sender attribution and default output style. Both /chat/<token>/ and /hook/<token> accept any valid token — kind is metadata, not a URL access gate. Mechanics shared at the handler.

columntypenullmeaning
token_hashBLOB PKNOT NULLsha256 of the raw token; raw token returned once at issuance
jidTEXTNOT NULLweb:<folder>[/<suffix>] or hook:<folder>/<source>[/<suffix>]<folder> is the destination, distinct from owner_folder
owner_folderTEXTNOT NULLissuing folder; bounds revocation
created_atTEXTNOT NULLRFC3339
contextTEXTNULLoptional per-link processing instructions; snapshotted onto each inbound as messages.link_context, rendered to the agent as <link-context>

Indexes: route_tokens_jid on jid.

Writers: routd (insertRouteToken, called from both MCP issue_chat_link/issue_webhook and REST POST /v1/route_tokens/*).

Readers: webd (hash + lookup on every /chat/* and /hook/* request); routd (list, revoke).

web_routes

webd's reverse-proxy route table: path_prefixfolder with an access mode.

columntypenullmeaning
path_prefixTEXT PKNOT NULLe.g. /dash/
accessTEXTNOT NULL CHECK IN ('public','auth','deny','redirect')
redirect_toTEXTNULLdestination when access='redirect'
folderTEXTNOT NULLowning folder (ACL key)
created_atTEXTNOT NULLRFC3339

Writers: store/web_routes.go:25 (UpsertWebRoute), :40 (DELETE).

Readers: webd.

Migrations: created 0045.

4. Onboarding & invites

onboarding

Admission state machine. A new sender lands here; gates and tokens flow through this row.

columntypenullmeaning
jidTEXT PKNOT NULLtyped JID of the unknown sender
statusTEXTNOT NULL'awaiting_message' | 'queued' | 'approved' | etc.
prompted_atTEXTNULLRFC3339
createdTEXTNOT NULLRFC3339
tokenTEXTNULLopaque admission token (0024)
token_expiresTEXTNULLRFC3339
user_subTEXTNULLlinked auth_users.sub on claim
gateTEXTNULLgate name that admitted (0027)
queued_atTEXTNULLRFC3339 (0027)

Indexes: idx_onboarding_token on token.

Writers: store/onboarding.go (queue mgmt), onbod/main.go:235/:251/:321/:571/:580/:659.

Readers: onbod (queue + admission check), dashd (operator queue).

Migrations: created 0009; 0021 collapsed flow; 0023 dropped sender/world_name/channel; 0024 added token/user_sub; 0027 added gate/queued_at; 0042 rewrote rows to typed JID.

onboarding_gates

Per-gate admission limits.

columntypemeaning
gateTEXT PKgate name
limit_per_dayINTEGER NOT NULLcap
enabledINTEGER NOT NULL DEFAULT 1on/off

Writers: store/onboarding.go:43 (upsert), :52 (DELETE), :62 (enable/disable).

Readers: onbod, dashd.

Migrations: created 0029.

invites

Invite tokens for the onboarding flow. Hard cutover of older invitations table in 0032.

columntypenulldefaultmeaning
tokenTEXT PKNOT NULLopaque token
target_globTEXTNOT NULLfolder glob the invite admits to
issued_by_subTEXTNOT NULLissuer auth_users.sub
issued_atTEXTNOT NULLRFC3339
expires_atTEXTNULLRFC3339
max_usesINTEGERNOT NULL1cap
used_countINTEGERNOT NULL0counter

Writers: store/invites.go:55 (INSERT), :119 (DELETE), :138 (UPDATE used_count).

Readers: onbod, cmd/arizuko (arizuko invite).

Migrations: created 0032 (replaces dropped invitations from 0028).

5. Secrets

Per-folder MCP rule overlays (formerly the grant_rules table, dropped in 0053) now live in acl as (principal=folder:<f>, action=mcp:<tool>, scope=<f>, params, effect) rows. Tier defaults are still derived in code by grants.DeriveRules (used as a fall-back for mcp:* actions with no acl match).

secrets

Folder- and user-scoped secret values. Encrypted at rest (AES-256-GCM, v2:-prefixed) under the SECRETS_KEY keyring; routd owns this table and refuses to start without the key (spec 7/Y).

columntypemeaning
scope_kindTEXT NOT NULL'folder' or 'user'
scope_idTEXT NOT NULLfolder path or auth_users.sub
keyTEXT NOT NULLenv-var name
valueBLOB NOT NULLAES-256-GCM ciphertext, v2:-prefixed (column renamed from enc_value in 0047)
created_atTEXT NOT NULLRFC3339

PK: (scope_kind, scope_id, key).

Indexes: partial idx_secrets_folder on scope_id WHERE scope_kind='folder'; partial idx_secrets_user on scope_id WHERE scope_kind='user'.

Writers: store/secrets.go:55 (upsert), :119 (DELETE), dashd/me_secrets.go:133 (user-scope INSERT), :174 (UPDATE), :209 (DELETE).

Readers: routd (FolderSecretsResolved, UserSecrets at spawn), dashd.

Migrations: created 0034 (originally enc_value BLOB); 0047 renamed to value.

secret_use_log

Audit log for the secret broker (spec 7/Y). One row per tool-call × resolved key. No secret values stored.

columntypedefaultmeaning
tsTEXT NOT NULLRFC3339Nano
spawn_idTEXT NOT NULL''
caller_subTEXT NOT NULL''resolved user sub
folderTEXT NOT NULL''spawn folder
toolTEXT NOT NULLMCP tool name
keyTEXT NOT NULLsecret key name
scopeTEXT NOT NULL'folder' or 'user'
statusTEXT NOT NULL'ok' / 'denied' / 'missing'
latency_msINTEGER NOT NULL0

Indexes: idx_secret_use_log_ts on ts.

Writers: store/secret_use_log.go:25.

Readers: dashd (audit views).

Migrations: created 0048.

6. Egress

network_rules

Per-folder egress allowlist; resolved root-to-folder by store.ResolveAllowlist at container spawn and consumed by crackbox.

columntypedefaultmeaning
folderTEXT NOT NULLfolder path; '' = root seed
targetTEXT NOT NULLhost (e.g. api.github.com)
created_atTEXT NOT NULLRFC3339
created_byTEXT NOT NULL'''system' / sub

PK: (folder, target). Indexes: idx_network_rules_folder on folder.

Seed rows: ('', 'anthropic.com'), ('', 'api.anthropic.com') at migration time.

Writers: store/network.go:18 (INSERT OR IGNORE), :27 (DELETE).

Readers: routd, dashd, cmd/arizuko (arizuko network).

Migrations: created 0037.

7. Scheduling

scheduled_tasks

Cron- and interval-scheduled prompts (driven by timed).

columntypenulldefaultmeaning
idTEXT PKNOT NULL
ownerTEXTNOT NULLowning folder
chat_jidTEXTNOT NULLtyped chat JID (rewritten in 0043)
promptTEXTNOT NULLinjected text
cronTEXTNULLcron expr / interval ms / empty = one-shot
next_runTEXTNULLRFC3339
statusTEXTNOT NULL'active''active' | 'paused' | 'firing' | 'completed'
created_atTEXTNOT NULLRFC3339
context_modeTEXTNOT NULL'group''group' | 'isolated' (0011)

Indexes: idx_tasks_next on (status, next_run).

Writers: store/tasks.go:27 (INSERT), :81 (UPDATE), :86 (DELETE), timed/main.go:96 (status transitions during fire).

Readers: timed, routd, ipc (task MCP tools), dashd.

Migrations: created 0001; 0011 added context_mode; 0043 rewrote chat_jid to typed shape.

task_run_logs

Audit log for every scheduled-task fire.

columntypemeaning
idINTEGER PK AUTOINCREMENT
task_idTEXT NOT NULL REFERENCES scheduled_tasks(id) ON DELETE CASCADE
run_atTEXT NOT NULLRFC3339
duration_msINTEGER NULL
statusTEXT NOT NULL'ok' / 'error'
resultTEXT NULL
errorTEXT NULL

Indexes: idx_task_run_logs_task on task_id.

Writers: timed/main.go:86.

Readers: dashd, ipc (list_task_runs).

Migrations: created 0011.

8. Misc

router_state

Generic key-value scratch for the gateway (per-key strings).

columntypemeaning
keyTEXT PK
valueTEXT NOT NULL

Writers: store/sessions.go:45.

Readers: routd.

Migrations: created 0001.

config_meta

Single-row optimistic-lock version for YAML config apply (spec 5/8). Each apply bumps version once per transaction; a CAS comparison inside BEGIN IMMEDIATE rejects stale applies. Bootstrap value counts existing config rows so a fresh apply against a populated DB doesn't reject with "version 0".

columntypedefaultmeaning
versionINTEGER NOT NULL0monotonic config version

Writers: routd (YAML apply, inside the config transaction).

Readers: routd (CAS on apply).

Migrations: created 0067.

group_watchers

Directional cross-folder ambient context for the observe_group MCP tool (spec 5/F). One row per (observer, source) pair; the observer receives messages delivered to the source as <observed> context on its next trigger turn.

columntypemeaning
observerTEXT NOT NULLfolder receiving context
sourceTEXT NOT NULLfolder being observed

PK: (observer, source).

Writers: ipc (observe_group / unobserve_group).

Readers: gateway (context assembly on trigger).

Migrations: created 0063.

cost_log

One row per LLM call (Anthropic plus oracle subcalls); the gateway aggregates daily totals for per-folder and per-user budget enforcement (spec 5/34).

columntypedefaultmeaning
tsTEXT NOT NULLRFC3339
folderTEXT NOT NULL''charged folder
user_subTEXT NOT NULL'''' = channel-scoped turn
modelTEXT NOT NULLe.g. claude-opus-4-7, gpt-5
input_tokINTEGER NOT NULL0prompt tokens
cache_readINTEGER NOT NULL0cheaper cached input tokens
cache_writeINTEGER NOT NULL0cache-creation tokens
output_tokINTEGER NOT NULL0completion tokens
centsINTEGER NOT NULL0precomputed cost

Indexes: idx_cost_log_folder_ts on (folder, ts); idx_cost_log_user_ts on (user_sub, ts).

Writers: gateway (per call).

Readers: gateway (daily aggregation for caps), dashd.

Migrations: created 0049.

proxyd_routes

Runtime-mutable proxyd route table (spec 6/2), mirroring the in-memory Route struct. proxyd seeds it from PROXYD_ROUTES_JSON on first boot when the table is empty; thereafter the table is authoritative and the env var is ignored.

columntypedefaultmeaning
pathTEXT PKroute path prefix
backendTEXT NOT NULLbackend URL
authTEXT NOT NULL'public' | 'user' | 'operator'
gated_byTEXT NOT NULL''grant action gate
preserve_headersTEXT NOT NULL'[]'JSON array
strip_prefixINTEGER NOT NULL00/1

Writers: proxyd (seed + runtime mutation), routd (config apply).

Readers: proxyd.

Migrations: created 0050.

audit_log

Unified source of truth for security events and state changes across MCP / REST / CLI / gateway / cron / crackbox / agent surfaces (spec 5/I). Round-2 of the audit coverage plan; supersedes the split cli_audit / ipc_audit tables, which stop receiving writes but remain until every writer is on audit.EmitInTx.

columntypemeaning
idINTEGER PK AUTOINCREMENT
created_atTEXT NOT NULLRFC3339 (millis)
categoryTEXT NOT NULLevent category
actionTEXT NOT NULLevent action
actorTEXT NOT NULLacting principal
actor_subTEXT NULLOAuth sub when known
resourceTEXT NULLtarget resource
scopeTEXT NULLscope for multi-level resources
surfaceTEXT NULLMCP / REST / CLI / …
params_summaryTEXT NULLredacted call summary
outcomeTEXT NOT NULL'ok' | 'error' | 'denied'
error_msgTEXT NULL
duration_msINTEGER NULL
turn_idTEXT NULLcorrelating turn
folderTEXT NULLaffected folder
instanceTEXT NULLinstance name
request_idTEXT NULLcross-daemon correlation
source_ipTEXT NULLREST source IP

Indexes: audit_log_created_at on created_at; partial audit_log_actor_sub / audit_log_folder (WHERE NOT NULL); audit_log_cat_act on (category, action).

Writers: audit.EmitInTx from every mutating surface.

Readers: dashd, the audit JSONL poll, forensic queries.

Migrations: created 0066.

cli_audit legacy — no new writes since 0066

Per-command CLI audit trail. Round-2 of the audit plan consolidated these rows into audit_log and stopped writing here; the table stays until the JSONL poll cursor migrates.

columntypemeaning
idINTEGER PK AUTOINCREMENT
tsDATETIME NOT NULLRFC3339 (millis)
os_userTEXT NOT NULLinvoking OS user
commandTEXT NOT NULLsubcommand
argsTEXT NOT NULLargument summary

Migrations: created 0061; superseded by audit_log (0066).

ipc_audit legacy — no new writes since 0066

Per-tool MCP mutation audit trail. Like cli_audit, its rows now flow into audit_log; the table is retained until every writer is migrated.

columntypemeaning
idINTEGER PK AUTOINCREMENT
tsDATETIME NOT NULLRFC3339 (millis)
folderTEXT NOT NULLcalling folder
subTEXT NOT NULLcalling sub
toolTEXT NOT NULLMCP tool name
paramsTEXT NOT NULLparam summary
outcomeTEXT NOT NULLcall result

Migrations: created 0062; superseded by audit_log (0066).

9. Tables removed by migrations

10. Audit of writers

There is no single schema owner: each split daemon (routd, runed, authd, onbod) migrates its own DB. The //go:embed migrations/*.sql in store/store.go:13 is the shared schema library each daemon draws from for the tables it owns. Per-table writer mapping above lists the canonical INSERT/UPDATE/DELETE call sites; multiple readers per table is the norm and noted as "many" where widespread.