dialect is "sqlite", "postgres" or "mysql". It picks the migration SQL, the same way provider does for the URL form. Everything else about the database section is unchanged: tables are created on boot, and skipMigrations: true makes boot fail with SchemaOutdatedError when the schema is behind (see Database setup).
What you own
theAuth does not open this connection, so it never closes it. Shut the pool down yourself when the process exits. Connection settings thatprovider applies for you become your job:
Requirements for the handle
- It must expose the driver client at
db.$client, because migrations and the schema check send raw SQL through it. The node-postgres, mysql2 and better-sqlite3 drivers do, and those are the ones this release tests. A driver that hides its client can still work if you run the migrations yourself (theauth db sql) and every query path goes through Drizzle, but boot calls the schema check even withskipMigrations, so that is untested territory. - Do not pass a
schemathat renames or replaces theAuth’s tables. theAuth uses its own table objects and never readsdb.query, so a plaindrizzle(pool)is enough. - Reads must see your own writes. Single use tokens, refresh rotation and the audit chain read right after a write. If you route reads to a replica, pin auth traffic to the primary.
createTheAuth returns your handle as auth.db, and plugins receive it as ctx.db, so it is the same object you passed in.
Hot path indexes
Migration0014 adds seven indexes that agent token lookup, the delegation walks, the hourly rate limit read and the pending approval list depend on. Without them each of those scans its whole table. If you own the schema and skip theAuth migrations, create them yourself. Keep the names, so a later run of the migration finds them and leaves them alone.
The SQL for each dialect:
@glinr/theauth into your own Drizzle schema, they already declare these indexes (SQLite dialect), so drizzle-kit generate emits them.
Typed unique conflicts
Several flows use a unique index as the tiebreaker: creating a user, appending to the audit chain, a second vote from the same approver, banning twice, MCP client and revocation inserts, erasure tombstones. They now map a driver error throughtoStoreConflict, which returns a StoreConflictError (exported from @glinr/theauth, with code: "STORE_CONFLICT", model and the driver error as cause).
Detection reads structured codes first: Postgres 23505, MySQL ER_DUP_ENTRY or errno 1062, SQLite SQLITE_CONSTRAINT_UNIQUE and SQLITE_CONSTRAINT_PRIMARYKEY, SQL Server 2627 and 2601, Prisma P2002, MongoDB 11000. The old text match stays as a fallback for drivers that wrap the code away, so behavior on the existing providers is unchanged.
Run the contract suite against your backend
@glinr/theauth-test-utils/store-contract holds the checks that make a backend safe for auth: 24 parallel callers fighting over one single use row with exactly one winner (magic link, email OTP, approval decision and consumption, refresh rotation), the attempt limit on OTP guesses, unique conflicts, and an audit chain that stays gapless and verifies when two processes append for one agent.
peer (a second instance with its own handle on the same database) to include the two process audit check, duplicateInsert to check conflict typing with your driver, and tokenFamilies for the refresh rotation race. The repository runs this suite on sql.js, better-sqlite3 through both config forms, Postgres and MySQL; set THEAUTH_TEST_PG_URL and THEAUTH_TEST_MYSQL_URL to include the last two.
The suite needs
vitest as a dev dependency. It runs against the instance’s public surface, so it describes how today’s Drizzle code behaves, not a future storage interface.