Storage adapters
Choose and configure the PostgreSQL, SQLite, or libSQL/Turso adapter, move a deployment between databases with snapshots, and read what doctor reports.
Better IAM stores everything through one small contract, IamStore, and ships three reference
that implement it on top of a shared RecordStore base. They behave identically: the same strict filters, the same
code-point ordering, the same serialized transactions. Pick the database for its operational properties, and move
between them later with a snapshot if your needs change.
| Adapter | Use it for | Driver (bundled) |
|---|---|---|
| PostgreSQL | Production deployments with sustained concurrent writes or large datasets; several application instances. | pg |
| SQLite | Single-host deployments, development, and tests. | better-sqlite3 (native) |
| libSQL | Local files, encrypted files, embedded replicas, and remote Turso or sqld databases. | @libsql/client |
Configure an adapter
Create the adapter and pass it to betterIam({ database }). Each adapter takes only the settings its database
needs:
import { postgresAdapter } from 'better-iam/adapter-postgres';
const database = postgresAdapter({
connectionString: process.env.DATABASE_URL!,
poolSize: 10,
});Prop
Type
Transactions serialize through a transaction-scoped advisory lock, which also covers other adapter instances
against the same database. Keep synchronous_commit on, and run VACUUM ANALYZE iam_records after a bulk import.
describe() reports the server version, synchronous_commit, the database size, and the lock timeout.
Every adapter is also available as its own package (@better-iam/adapter-postgres,
@better-iam/adapter-sqlite, @better-iam/adapter-libsql). Durability, indexes, and upgrade windows are covered
in Database operations. To write your own adapter, see
Adapters and plugins.
Snapshots and moving between databases
Deployments often start on SQLite and outgrow it, or need a portable backup that does not depend on one database's
tooling. store-export, store-import, and store-copy move a whole deployment between databases and adapters,
for example from SQLite to PostgreSQL. They are deployment operations that work on storage directly and need no
credential.
better-iam store-copy --config better-iam.config.mjs --target-config target.config.mjs
better-iam migrate --config target.config.mjsstore-copy copies the configured database into the empty database of --target-config (which must be a
different configuration file) in one step: one read transaction on the source and one write transaction on the
target, so the copy is consistent and all-or-nothing.
- Consistency. The export reads in one transaction, so the snapshot is consistent, but that also holds the write lock until it finishes. Schedule large exports accordingly.
- All or nothing. A truncated or corrupt snapshot, a count that disagrees with the trailer, or a record the database refuses rolls the whole import back.
- Verbatim records. Password hashes, sessions, encrypted secrets, and audit chains stay valid as long as the
target configuration uses the same
secret. - After loading, run
migratewith the target configuration to apply plugin migrations. On PostgreSQL, follow a bulk import withVACUUM ANALYZE iam_records.
Protect snapshots like the database
A snapshot holds credential hashes and encrypted secrets. Store and transfer it with the same care as the database and its secret.
The same operations are available in code as exportStore, importStore, and copyStore from
better-iam/core:
import { copyStore } from 'better-iam/core';
import { postgresAdapter } from 'better-iam/adapter-postgres';
import { sqliteAdapter } from 'better-iam/adapter-sqlite';
const source = sqliteAdapter({ filename: './iam.db' });
const target = postgresAdapter({ connectionString: process.env.DATABASE_URL! });
await target.migrate();
const summary = await copyStore(source, target); // { records, collections: { [name]: count } }Doctor
Many deployment mistakes are silent: a schema one migration behind, a placeholder secret, a scheduler that stopped
weeks ago. doctor looks for them in one read-only pass, so you can run it after every deploy and on a schedule.
better-iam doctor connects to the configured database and prints a JSON report with:
- the Node version, whether the root is initialized, and the number of chained tenants and audit events;
- the adapter's own view under
storage(IamStore.describe()): schema version, applied migrations with their times, record counts per collection, and settings such as SQLite's journal mode, synchronous level, and file size, or PostgreSQL's server version andsynchronous_commit; findingsfromiam.selfCheck(), the same check your application can run in code.
better-iam doctor --config better-iam.config.mjs --strict --retention-days 30doctorexits 0 whenever it can connect, including to a database without the IAM schema.--strictexits non-zero (DOCTOR_FINDINGS) on any error or warning, for example as a deployment gate.- Pass the
--retention-daysyour sweep uses (default 30), ordeliveryRetentionMsandgraceMstoselfCheck, so the backlog is judged the same way the sweep would. - The checks only read.
selfCheck({ cap })bounds how many records each backlog check counts (default 1000). - Each finding has a stable
checkname, aseverity, amessage, and afix.okis false when any finding is an error.
| Check | Severity | What it means |
|---|---|---|
schema-behind | error | The database lacks a migration of this release. Run migrate. |
not-bootstrapped | error | No root tenant exists yet. |
sqlite-durability | error | SQLite runs a rollback journal at durability: 'normal', which a power failure can corrupt. |
unreadable-secrets | error | Stored values open with no configured secret. See Secrets and keys. |
in-memory-database | warning | Everything is lost when the process exits. |
postgres-async-commit | warning | PostgreSQL runs with synchronous_commit = off. |
weak-secret | warning | The secret looks like a placeholder or has little variety. |
weak-metrics-token | warning | The metrics bearer token is shorter than 24 characters. |
no-email-transport | warning | No sendEmail transport is configured. |
secret-rotation-pending, secret-rotation-unverified | warning | A secret rotation is not finished. |
sweep-backlog | warning | Records have been due for the retention sweep for more than two days. |
purge-not-running | warning | Expired bindings, memberships, or challenges are more than a day old. |
outbox-stalled | warning | Outbox messages have waited more than 15 minutes. |
outbox-abandoned | warning | Messages were abandoned in the last day after repeated failures. |
audit-hooks-stalled | warning | Audit events have waited more than 15 minutes for plugins, subscribers, or onEvent. Dispatch them in the process that registers subscribers. |
audit-archive-behind | warning | A tenant has unarchived audit events older than a day. |
previous-secrets-configured | info | previousSecrets can be removed. |
storage-undescribed | info | The adapter cannot describe itself, so migrations were not checked. |
The job-related warnings are the fastest way to notice a scheduler that stopped; see Scheduled jobs.
Next steps
Better IAM is created by Sean Filimon
Last updated
Build and release
How a Better IAM release is checked, packed, smoke-tested as installed tarballs, versioned in lockstep, and prepared for publication.
Scheduled jobs
The worker jobs a deployment schedules (outbox, purge, sweep, reconcile, digest, remind, certifications, invariants, audit archive) with cadences and results.