Proposed.
Axon’s server-side archive store has depended on Postgres since the start of the project (ADR 0003). We did not make that choice after an extended Postgres vs. SQLite design discussion; it was the straightforward fit for a Rust server with a durable event archive, JSON-heavy Matrix payloads, migrations, and a future path to hosted or larger deployments.
The product shape now makes the tradeoff worth recording explicitly. Axon is primarily a self-hosted personal agent: one human per process, with one or more Matrix accounts inside. For that model, requiring Docker or a separate Postgres service is a real adoption cost. SQLite would make the default path closer to a single binary plus a local data directory.
At the same time, we do not want to support two archive-store backends. Two SQL dialects would double the correctness surface for the parts of Axon where the database is most load-bearing: lifecycle state, message history, relation aggregation, media lookup, search convergence, and account-token storage.
Keep Postgres as the archive store for the near-term MVP. Do not add a second SQLite backend.
If “no Docker / no separate database server” becomes a hard pre-MVP product requirement, make SQLite a complete replacement for Postgres in a dedicated storage milestone, before adding more store-heavy features. That switch must rewrite the store, migrations, tests, launcher scripts, and token-encryption mechanism as one coherent change; it should not be introduced as a compatibility layer.
The recommendation is therefore:
The current code is not database-neutral. axon-store owns a PgPool, all row
decoding is typed to PgRow / Postgres, and axon-server and the token CLI
connect through Store::connect(&config.database.url, ...). The configuration
surface documents database.url as a required Postgres URL, and the local
developer path centers on docker-compose.yml, .env, and DATABASE_URL.
The schema and query layer depend on Postgres behavior in several important places:
UUID, JSONB, TIMESTAMPTZ,
BIGSERIAL, BYTEA, Postgres expression indexes, partial indexes, and
plpgsql triggers for updated_at.pgcrypto (pgp_sym_encrypt / pgp_sym_decrypt) and enables the extension in
the baseline migration. Bearer tokens are already hashed in Rust, but Matrix
access tokens still rely on pgcrypto.LEFT JOIN LATERAL, DISTINCT ON, array_agg,
bool_or, aggregate FILTER, IS DISTINCT FROM, and jsonb_* constructors.
This is the hot path for edit collapse, reaction tallies, redaction masking,
replies, and thread reads.search_outbox writes append
indexing obligations in the same statement as event mutations using writable
CTEs with RETURNING. That is central to the “persisted event and indexing
obligation commit atomically” guarantee.content->'info'->'thumbnail_file'->>'url'.DATABASE_URL; integration scripts use psql for
setup and assertions.sqlx-sqlite because it
conflicted with the Matrix SDK’s rusqlite / libsqlite3-sys version at the
time. The current lockfile has the SDK path on rusqlite and
libsqlite3-sys; a SQLite switch must re-check this dependency story rather
than assume sqlx-sqlite can be enabled trivially.None of these are impossible to port. Together, they make switching a storage milestone, not a config change.
Postgres fits Axon’s current implementation and future headroom:
The cost is operational: users must run a DB service, manage credentials, avoid port conflicts, back it up, and understand Docker or local Postgres.
SQLite is attractive for Axon’s personal, single-human deployment model:
UPSERT, RETURNING, and STRICT tables.The cost is architectural: SQLite has one writer at a time in WAL mode, no standard pgcrypto equivalent, different JSON/type semantics, and a different SQL dialect for the exact queries Axon currently relies on.
Switching now would be high but bounded. It is still cheaper now than after history backfill, more search API work, or additional materialized read models land, but it is not a small refactor.
A credible SQLite replacement must include:
rusqlite to align with the Matrix SDK, or a
proven sqlx-sqlite dependency plan that resolves cleanly with the SDK).PRAGMA foreign_keys = ON, WAL configuration, busy timeouts, and a checkpoint
policy.sync.store_key, with a key/version
envelope that preserves the later rotation path ADR 0008 already calls out.DISTINCT ON, LATERAL, Postgres arrays,
JSONB constructors, and FILTER-based JSON aggregation need SQLite equivalents
or staged Rust-side assembly. The result must keep ADR 0033’s semantics exactly.RETURNING, but its docs do not allow DML-with-RETURNING to be used as a
table expression in the same way Axon’s Postgres writable CTEs are used today.
The atomic “mutation plus outbox obligation” guarantee must be preserved with
explicit transactions or triggers..env, launchers, integration scripts, docs, and test helpers
so the default data path is file-based rather than DATABASE_URL.The safest implementation shape would be a clean branch that replaces the
Postgres store wholesale, ports the existing DB-gated tests to temp SQLite files,
and keeps the public /v1/ API unchanged.
SQLite does not weaken Axon by definition, but it moves several responsibilities from the database/service boundary into the application:
-wal and -shm sidecar files; backup documentation must account
for them or use the SQLite backup API.sqlx-sqlite dependency issue.search_outbox.RETURNING documentation:
https://www.sqlite.org/lang_returning.html