Springtale uses one SQLite file
($SPRINGTALE_DATA_DIR/springtale.db) encrypted at rest with
SQLite3MultipleCiphers (ChaCha20-Poly1305). This is everything the
daemon persists except the vault: rules, events, formation state,
mental models, audit trail, webhook secrets, runtime config.
17 schema files in crates/springtale-store/src/schema/sql/, applied
declaratively at boot via schema/apply.rs. The split:
| File | Tables | Domain |
|---|---|---|
ai_token_usage.sql |
ai_token_usage |
per-bot daily AI token counters (quota / observability) |
approvals.sql |
pending_approvals, tool_loop_checkpoints |
approval-gate decisions + paused tool loops awaiting approval |
audit.sql |
audit_trail |
every sentinel verdict (hash-chained rows) |
bot.sql |
bot_sessions, user_prefs, bot_memory, bot_aliases |
session memory, prefs, aliases |
connectors.sql |
connectors |
installed connectors + their config |
cooperation.sql |
coop_writes, coop_deposits, mental_model_* |
cooperation state that crosses dissolves |
dedupe.sql |
dedupe_seen |
Action::Dedupe seen-keys (blake3 digests only) |
events.sql |
events |
trigger fires + dispatch outcomes |
execution.sql |
execution_results |
legacy rule execution history |
executions.sql |
executions, execution_steps |
per-chain-fire observability (Phase B; sizes only, no payloads) |
formations.sql |
formations, formation_members, formation_momentum, formation_rally |
formation rosters + momentum + rally tokens |
jobs.sql |
jobs |
scheduled work (in-memory mpsc for now) |
mental_model_workspaces.sql |
mental_model_workspaces |
discovered external chat destinations (D1) |
rules.sql |
rules |
rule definitions |
runtime_config.sql |
config_store |
hot-swappable config (AI provider, etc.) |
safety.sql |
safety_config |
disguise, quick-hide, panic-tap settings |
wasm.sql |
wasm_binaries |
content-addressed WASM connector blobs |
The schema version (PRAGMA user_version) is currently 1. If the
daemon boots against a database with a different user_version, it
refuses to start and reports E004. See upgrade.md.
You can't open it with stock sqlite3 — the encryption-at-rest layer
isn't in stock SQLite. Three options:
The CLI exposes inspection commands that go through the encrypted layer automatically:
springtale-cli rule list
springtale-cli connector list
springtale-cli events --limit 50
springtale-cli memory auditFor ad-hoc SQL, the daemon doesn't expose it directly (no eval endpoint on purpose). Add a one-off subcommand if you really need it.
The libsqlite3-sys-mc shim in crates/ is the vendored SQLite3MultipleCiphers
build. With that linked:
sqlite3mc "$SPRINGTALE_DATA_DIR/springtale.db"
PRAGMA key='<your derived key>';
SELECT * FROM rules LIMIT 5;Getting the derived key from the passphrase requires running the same Argon2id KDF Springtale uses. Easier path:
springtale-cli data export --output dump.jsonJSON, not SQL — but structured by table. Load into a regular SQLite file if you want SQL access:
# requires writing a small import script. JSON-to-SQLite is a few
# lines of Python or jq + sqlite3. The export is stable, documented
# at docs/reference/data-format.md (when we ship that).For a one-off, just jq the JSON directly.
| Table | Growth rate | Bound |
|---|---|---|
events |
linear with trigger volume | none — manual purge |
audit_trail |
linear with action volume | [sentinel] audit_retention_days |
bot_memory |
linear with bot conversation length | [bot] context_window per session |
execution_results |
linear with rule fires | none — manual purge |
formation_momentum |
one row per formation per ~tick | grows fast for long-running formations |
mental_model_* |
one row per (formation, observation type, …) | growth model varies; bounded per formation |
connector_outputs |
bounded — rolling buffer of last 100 per connector | hard cap, oldest dropped |
The two tables that grow fastest in production are events and
audit_trail. Both have configurable retention. Set sensible bounds:
[sentinel]
audit_retention_days = 90Events retention isn't yet exposed in config (tracked in AUDIT-NOTES.md §9). For now:
sqlite3mc-via-cli "DELETE FROM events WHERE created_at < datetime('now', '-30 days')"SQLite reclaims space lazily. After large deletes, run:
springtale-cli data vacuumOr, if you've stopped the daemon:
sqlite3mc "$SPRINGTALE_DATA_DIR/springtale.db" "VACUUM;"Vacuum holds an exclusive lock for the duration — schedule it for low activity. Typical reclamation rates after a 30-day events purge: 60–80% of file size returned.
The database runs in WAL (Write-Ahead Log) mode for two reasons:
- Readers don't block writers. The dashboard can read formations while the bot writes momentum updates.
- Crash safety. WAL is replayed on next open; partial writes are discarded.
WAL files (springtale.db-wal, springtale.db-shm) appear next to
the main .db while the daemon runs. After a clean shutdown they're
truncated/removed. After a crash they're replayed at next boot.
Never delete the WAL file while the daemon is running or has crashed without replaying. You'll lose every write since the last checkpoint.
The database encryption key is derived from your vault passphrase via Argon2id (separate KDF from the AEAD vault key, distinct salt). The derived key lives in process memory only — never on disk.
If you forget the passphrase, the database is unrecoverable. There is no key escrow, no recovery flow, no support hotline. This is by design.
Some inspection paths are surfaced as API endpoints + CLI:
springtale-cli memory audit # session memory summary
GET /sessions/{id}/memory # via the API
GET /events?since=...&limit=...
GET /audit-trail?since=...&limit=...See docs/reference/api.md for the full surface.
For incident response — pulling data out without a running daemon:
- Stop the daemon cleanly so WAL is checkpointed.
- Copy the entire data directory to a forensic-clean machine.
- Install Springtale on that machine with the same version as the source.
- Set
SPRINGTALE_DATA_DIRto the copied directory. - Run
springtale-cli data export --output dump.jsonwith the passphrase. - Analyse
dump.json.
Don't try to read the database file directly with stock SQLite tools. The encryption layer won't be there.