BetterIAM

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/adapter-postgres@better-iam/adapter-sqlite@better-iam/adapter-libsql@better-iam/core@better-iam/clideployment.mdextensions.mdrecipes.mdindex.tsindex.tsindex.tssnapshot.tsself-check.tsindex.ts

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.

AdapterUse it forDriver (bundled)
PostgreSQLProduction deployments with sustained concurrent writes or large datasets; several application instances.pg
SQLiteSingle-host deployments, development, and tests.better-sqlite3 (native)
libSQLLocal 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:

lib/iam.ts
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.mjs

store-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 migrate with the target configuration to apply plugin migrations. On PostgreSQL, follow a bulk import with VACUUM 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 and synchronous_commit;
  • findings from iam.selfCheck(), the same check your application can run in code.
better-iam doctor --config better-iam.config.mjs --strict --retention-days 30
  • doctor exits 0 whenever it can connect, including to a database without the IAM schema. --strict exits non-zero (DOCTOR_FINDINGS) on any error or warning, for example as a deployment gate.
  • Pass the --retention-days your sweep uses (default 30), or deliveryRetentionMs and graceMs to selfCheck, 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 check name, a severity, a message, and a fix. ok is false when any finding is an error.
CheckSeverityWhat it means
schema-behinderrorThe database lacks a migration of this release. Run migrate.
not-bootstrappederrorNo root tenant exists yet.
sqlite-durabilityerrorSQLite runs a rollback journal at durability: 'normal', which a power failure can corrupt.
unreadable-secretserrorStored values open with no configured secret. See Secrets and keys.
in-memory-databasewarningEverything is lost when the process exits.
postgres-async-commitwarningPostgreSQL runs with synchronous_commit = off.
weak-secretwarningThe secret looks like a placeholder or has little variety.
weak-metrics-tokenwarningThe metrics bearer token is shorter than 24 characters.
no-email-transportwarningNo sendEmail transport is configured.
secret-rotation-pending, secret-rotation-unverifiedwarningA secret rotation is not finished.
sweep-backlogwarningRecords have been due for the retention sweep for more than two days.
purge-not-runningwarningExpired bindings, memberships, or challenges are more than a day old.
outbox-stalledwarningOutbox messages have waited more than 15 minutes.
outbox-abandonedwarningMessages were abandoned in the last day after repeated failures.
audit-hooks-stalledwarningAudit events have waited more than 15 minutes for plugins, subscribers, or onEvent. Dispatch them in the process that registers subscribers.
audit-archive-behindwarningA tenant has unarchived audit events older than a day.
previous-secrets-configuredinfopreviousSecrets can be removed.
storage-undescribedinfoThe 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

Was this page helpful?

Better IAM is created by Sean Filimon

Last updated

On this page