Adapters
WIPThe stores an engine persists through, the shipped adapters, mixing Redis sessions with SQL, transactions, limiters, and writing your own
An adapter is three stores: identities, credentials and sessions. Pass one as stores:
import { drizzlePgAdapter } from '@gentleduck/auth/adapters/drizzle/pg'
const auth = createAuth({
baseUrl: 'https://app.example.com',
stores: drizzlePgAdapter(process.env.DATABASE_URL ?? ''),
// ...
})
| Adapter | Import | Holds | For |
|---|---|---|---|
| Memory | adapters/memory | All three, and orgs | Tests and local runs. strict() refuses it in production. |
| Drizzle | adapters/drizzle/{pg,mysql,sqlite} | All three | Production. |
| Redis and Valkey | adapters/redis, adapters/valkey | Sessions, events, nonces | Sessions across instances, and the shared state beside them. |
An orgs store is yours to write over your own tables.
Mixing stores
Each store is independent, so sessions can live in Redis beside identities in SQL:
import { session } from '@gentleduck/auth/adapters/redis'
const sql = drizzlePgAdapter(db)
createAuth({
// ...
stores: { identities: sql.identities, credentials: sql.credentials, sessions: session({ redis }) },
})
Transactions
auth.withTransaction(tx) answers the engine bound to your transaction handle, so its writes commit or roll back with yours. Its events wait in pending, so flush them once the transaction commits:
await db.transaction(async (tx) => {
const bound = auth.withTransaction(tx)
const ada = await bound.identities.create({ profile: { email: 'ada@example.com', username: 'ada' } })
await tx.insert(accounts).values({ ownerId: ada.id })
return bound.pending
}).then((pending) => pending.flush())
Only a Drizzle adapter passed whole can join one. A hand-built mix and the memory adapter throw AUTH_MISCONFIGURED.
Limiters
The limiter is what every sign-in, link request and challenge spends from. Pass one as limiter:
| Limiter | Import | Notes |
|---|---|---|
redisLimiter({ redis }) | limiters/redis | Shared across instances. Default 10 per 15 minutes per key. |
valkeyLimiter({ redis }) | limiters/valkey | The same over an ioredis or iovalkey client. |
new MemoryLimiter() | limiters/memory | One process only. |
noopLimiter() | limiters/mock | Never refuses. Tests only. |
Without one, the engine counts in a MemoryLimiter. strict() refuses that in production, and the noop one: a limiter per process grants every budget once per instance.
Writing your own
Implement Identities.Store, Credential.Store and Sessions.Store from @gentleduck/auth/core. The contract each method keeps is on its type.
- A missing row rejects with its not-found code, such as
AUTH_IDENTITY_NOT_FOUND; it never answersnull. - A stale
expectedVersionrejects withAUTH_STALE_WRITE, and a taken email, username or login with its own code. - Extend
AdapterStorefrom@gentleduck/auth/adaptersand call throughrun: it maps what your driver throws onto the codes you declare, and anything else toAUTH_ADAPTER_FAILED. - Give it a
withClient(tx)answering the same stores on a transaction handle, and it can joinwithTransaction.