Data
The aeqi runtime's storage model. Two SQLite databases per tenant. No Postgres, no Redis, no message queue.
Two databases
| Database | Tables | What |
|---|---|---|
aeqi.db |
entities, agents, roles, role_edges, ideas, events, channels, credentials, runtime_placements (in some deployments) |
The state substrate — who, what, where, why. |
sessions.db |
sessions, session_participants, session_messages, quests |
The execution substrate — conversations, work, transcripts. |
Quests live in sessions.db (NOT aeqi.db — common mistake). Ideas live in aeqi.db. The repo-root agents.db is a 0-byte stub from the legacy schema; don't touch it.
Core tables (aeqi.db)
entities
entities(
id TEXT PRIMARY KEY, -- UUID; the workspace/TRUST id
display_name TEXT,
blueprint TEXT, -- e.g. "aeqi"
trust_address TEXT?, -- on-chain TRUST contract; NULL pre-registration
created_at INTEGER
)
Fresh entity_id at creation, distinct from any agent or role UUID. The runtime row lives here.
agents
agents(
id TEXT PRIMARY KEY,
entity_id TEXT NOT NULL REFERENCES entities(id),
name TEXT,
model TEXT?, -- preferred LLM
capabilities TEXT?, -- JSON tool permissions
status TEXT, -- 'active' | 'paused' | 'retired'
created_at INTEGER
)
Agents are entity-owned assets. parent_id is GONE (legacy). An agent's tree position is determined by which Roles it occupies and the role DAG.
roles + role_edges
roles(
id, entity_id, title, occupant_kind, occupant_id,
role_type -- 'director' | 'operational' | 'advisor'
)
role_edges(parent_role_id, child_role_id)
Authority via recursive CTE on demand. No ACL table. See Roles.
ideas
ideas(
id, entity_id, kind, name, content,
tags, properties, embedding,
created_at, updated_at, author_id,
session_id?
)
FTS5 virtual table for full-text search. Optional vector embeddings for semantic retrieval. Hybrid ranking combines BM25 + vector cosine + temporal decay.
events
events(
id, kind, source, payload,
fired_at, dispatched_at?,
scope_kind, scope_id
)
Events are queryable indefinitely but their first-class job is to fire actions. See Events.
channels
channels(
id, agent_id, kind, -- telegram | whatsapp | email | slack
bot_token (encrypted),
allowed_chats[],
on_mention, -- behavior: 'fire_turn' | 'spawn_from_template:<slug>'
config (JSON)
)
A channel attaches to an agent and bridges to sessions via gateway_channel_id.
credentials
credentials(
scope_kind, -- Entity | Role | Agent
scope_id,
provider, -- google | slack | github | stripe | telegram
name, -- 'oauth_token' | 'api_key' | etc.
data (encrypted)
)
Lookup precedence: Agent > Role > Entity, narrowest wins. See Multi-scope integrations.
Core tables (sessions.db)
sessions
sessions(
id, agent_id?, session_type, name, status,
gateway_channel_id?, -- bridge to a channel (Telegram, etc.)
awaiting_at?, -- legacy; deprecation Wave 5
created_at, updated_at
)
A session is multi-participant; participants live in their own table.
session_participants
session_participants(
session_id, identity_kind, -- user | agent | role | external
identity_id,
joined_at, joined_by,
history_visibility, -- 'full' default
PRIMARY KEY (session_id, identity_kind, identity_id)
)
Adding someone is an explicit verb. See Sessions.
session_messages
session_messages(
id, session_id,
from_kind, -- user | agent | role | system
from_id,
role, -- legacy llm role tag (user/assistant/...)
content, payload?,
timestamp, sender_id
)
from_kind discriminates message author. from_kind=system → activity row (state changes, tool calls). from_kind ∈ {user, agent, role} → comment/message.
quests
quests(
id, entity_id, agent_id,
subject, description,
status, -- pending | in_progress | done | blocked | cancelled
priority,
parent_id?, depends_on?,
idea_id?, -- the artifact this work produces or refines
outcome?, -- one-line summary on close
created_at, updated_at
)
Quest wraps Idea. See Composition.
Indexes & FTS
ideas_fts5virtual table overideas.name + content.- B-tree on
events(kind, fired_at),quests(entity_id, status),session_messages(session_id, timestamp). - Optional
vec0index onideas.embeddingwhen embeddings are enabled.
Migrations
Forward-only migrations under crates/aeqi-runtime/migrations/. The runtime applies them at startup before opening the API. Never drop a column without a migration; never rename in-place.
Phase 4 (2026-04-29) retired:
agents.parent_idagent_directorstableagent_ancestryclosure table
These columns are gone. Don't add backwards-compat shims; fresh spawns mint three distinct UUIDs (entity, agent, role).
Backups
A backup is a file copy. Stop writes briefly (or use SQLite's online backup API), copy the two .db files plus the encrypted credentials KEK wrap, done.
sqlite3 /var/lib/aeqi/<entity_id>/aeqi.db ".backup /backup/aeqi.db"
sqlite3 /var/lib/aeqi/<entity_id>/sessions.db ".backup /backup/sessions.db"
Restore is the reverse — drop the new files in place and restart the service.
Per-tenant isolation
Each tenant runtime has its own data directory; the platform proxy routes X-Entity headers to the right tenant and the runtime never sees other tenants' data. The data directory's location depends on whether the tenant runs sandboxed or in-host:
| Placement | systemd unit | Data directory |
|---|---|---|
| sandbox (containerized) | aeqi-sandbox-<entity_id>.service |
/var/lib/aeqi/containers/<entity_id>/aeqi.db |
| host (in-host) | aeqi-host-<entity_id>.service |
$HOME/.aeqi/aeqi.db (the unit sets HOME=/home/claudedev, so the runtime resolves data dir to ~/.aeqi) |
Operational note: a stale /var/lib/aeqi/hosts/<slug>/aeqi.db file may exist on hosts that ran the pre-2026-04-29 layout. No daemon opens it, so its schema doesn't auto-upgrade. When verifying a migration on a tenant DB, resolve the live path from the systemd unit (Environment=HOME and WorkingDirectory) rather than guessing — checking the dormant copy will mislead you.