Running Atom on SQLite
When to choose SQLite, how to configure it, its fixed durability policy, backup and restore, and how it differs from PostgreSQL.
Running Atom on SQLite
Atom stores its data in PostgreSQL or in a local SQLite file. The scheme of
DATABASE_URL selects the backend. Both backends share business rules and
authorization evaluation, with native SQL repositories and migrations for each
storage engine. The database contract tests run against both backends.
| PostgreSQL | SQLite | |
|---|---|---|
| Use for | multi-replica, high write rate, shared infrastructure | one process, one host: development, edge, appliance, small self-hosted installs |
DATABASE_URL | postgres://user:pass@host:5432/atom | sqlite:///var/lib/atom/atom.db |
| Processes per database | many | exactly one |
| Storage | server managed | one file on a local disk |
Configuration
- The directory must already exist and be writable by Atom; Atom creates the file on first start and applies the schema.
- Query parameters (
?mode=…) are rejected. Durability settings are fixed, not tunable. - Use a local filesystem. Network and shared storage (NFS, SMB, most container volume drivers that back onto them) do not provide the locking and fsync guarantees SQLite depends on, and are unsupported.
ATOM_DB_MAX_CONNECTIONSsets the pool size when present; otherwise a file database uses a small pool of 5 (SQLite has one writer, so a large pool only adds contention).
Run locally
Start from the development environment example if you do not already have an
.env file. The command below preserves an existing configuration:
The example contains development credentials and an encryption key. For a real
deployment, replace ADMIN_SECRET with a strong administrator password and
generate a unique ATOM_KEY_ENCRYPTION_KEY with openssl rand -base64 32. Save
that value securely in your deployment configuration. Atom generates its ES256
signing keys automatically; there is no JWT_SECRET.
Check readiness from another terminal:
PostgreSQL is not required for this configuration. Keep the same
ATOM_KEY_ENCRYPTION_KEY across restarts; replacing it makes existing encrypted
credentials and keys unreadable.
Fixed runtime policy
Every connection is opened with:
| Setting | Value | Why |
|---|---|---|
journal_mode | WAL | readers do not block the writer |
synchronous | FULL | a committed transaction survives power loss |
foreign_keys | ON | the schema's referential integrity is enforced |
recursive_triggers | ON | trigger semantics match PostgreSQL |
busy_timeout | 30 s | a contended write waits, then fails closed |
Every write transaction starts with BEGIN IMMEDIATE, so a transaction that
reads then writes can never deadlock against another writer. If the write lock
cannot be taken within the busy timeout, the request fails with HTTP 503 /
gRPC UNAVAILABLE ("database is busy; retry shortly") instead of hanging.
One process per database
Atom refuses to start if another Atom process already owns the database file. It
holds an exclusive lock on a sibling file, atom.db.atom-lock, for as long as it
runs, and tells you so:
This is a guard against a rolling deploy or a stray second instance corrupting the single-writer assumption. It is why SQLite is a single-node choice: to run several replicas, use PostgreSQL. Do not delete the lock file to "force" a start while another process is alive.
Files on disk
All four belong together. Never copy atom.db alone while Atom is running.
Backup and restore
Take a consistent backup with SQLite's own tooling; both commands are safe while Atom is running:
A plain file copy is only safe once Atom is stopped and the WAL has been folded
in; if you copy files rather than use the commands above, copy atom.db and
atom.db-wal together.
To restore, stop Atom, replace atom.db with the backup, delete any stale
atom.db-wal and atom.db-shm, and start Atom. The schema migration runs
idempotently on start. The atom.db.atom-lock file carries no data and can be
left in place.
Tables, indexes and views use only SQLite built-ins, so any SQLite tool can open, read and copy the database. A few invariant triggers call Atom's own SQL functions (
atom_*), which Atom registers on every connection; a write made from another client to those tables would fail. Treat direct writes as unsupported and use Atom's API.
Behaviour that differs from PostgreSQL
The application behaves the same; these are storage-engine differences to be aware of:
- Single writer. All writes serialize. Concurrent requests queue for the write lock rather than running in parallel; reads are unaffected.
- No advisory or row locks. Atom's PostgreSQL row/advisory locks exist to
order concurrent writers. On SQLite the single write lock already guarantees
the same serialization for mutations. SQLite repositories read and update
through the caller's
BEGIN IMMEDIATEtransaction instead of issuing PostgreSQL lock statements. - Data encodings. UUIDs are stored as 16-byte blobs, timestamps as fixed-width UTC text with microseconds, JSON as validated text, arrays as JSON text. These are internal; the API, events and exports are identical.
- JSON containment search (
attributesContains) scans instead of using a GIN index. That is fine at the scales SQLite is meant for. TRUNCATEdoes not exist; Atom usesDELETE.- CRL digest constraint. PostgreSQL additionally checks that a stored CRL's SHA-256 column equals the digest of its DER bytes. SQLite has no built-in SHA-256, so only the format is checked in the database; Atom computes the digest itself when it stores a CRL.
Choosing a backend
Pick SQLite when a single Atom process on one host is enough and you value a zero-dependency deployment. Pick PostgreSQL when you need more than one Atom replica, an external database team, managed backups/PITR, or sustained heavy write load.
Moving an existing deployment to PostgreSQL
Changing DATABASE_URL connects Atom to a different database; it does not
copy existing data. SQLite and PostgreSQL storage encodings and migration
histories are not interchangeable.
There is currently no complete database migration command. API/bootstrap provisioning can recreate supported configuration, but it is not a backup or full transfer of credentials, sessions, audit history, and event delivery state. A complete migration needs a separately implemented and validated export/import workflow, a maintenance window, and verification before accepting live writes on PostgreSQL. Keep the source backup and encryption keys until that migration is verified.