Skip to main content

Production hardening

WIP

A production engine, the order it has to be built in, and everything strict() refuses in production

import { drizzlePgAdapter } from '@gentleduck/auth/adapters/drizzle/pg'
import { valkeyEvents, valkeySessionImpl } from '@gentleduck/auth/adapters/valkey'
import { createAuth } from '@gentleduck/auth/core'
import { valkeyLimiter } from '@gentleduck/auth/limiters/valkey'
import { passwords } from '@gentleduck/auth/providers/passwords'
import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL ?? '')
const sql = drizzlePgAdapter(process.env.DATABASE_URL ?? '')

export const auth = createAuth({
  baseUrl: 'https://app.example.com',
  stores: { identities: sql.identities, credentials: sql.credentials, sessions: valkeySessionImpl({ redis }) },
  limiter: valkeyLimiter({ redis }),
  events: valkeyEvents({ cmd: redis, sub: new Redis(process.env.REDIS_URL ?? '') }),
  providers: [passwords()],
  strict: false,
})

auth.events.on('lockout', (event) => alertOps(event))
auth.strict({ env: 'production' })

createAuth runs strict() as it returns, for the env named by strict or else NODE_ENV, falling back to development. Production requires a lockout listener, which cannot exist before createAuth returns, so a production engine opts out there and calls strict() once it is subscribed.

Outside production, strict() checks only the compliance preset, if one is set. A failure throws one AUTH_MISCONFIGURED whose meta.detail lists every problem.

What production refuses

AreaRefused
LimiterNone, NoopLimiter, or the in-process MemoryLimiter.
StoresA memory store in any stores slot.
EventsNo bus, or the in-process one, while it holds a fleet handler.
CaptchaThe null verifier, which passes everything, or a siteverify endpoint over http.
TransportA cookie transport with secure: false, or a JWT transport signing HS256 with a key under 32 bytes. A composite's parts are checked one by one.
OAuthA stateSigningSecret under 32 bytes, or allowStateReplay: true.
PasskeyThe in-process challenge store.
Magic linkNo deliver, so no link could be sent.
PasswordsA hasher below its own defaults: scrypt N under 2^14, r under 8 or keylen under 32; Argon2id memoryCost under 19456, timeCost under 2 or hashLength under 32.
EngineA baseUrl that is not https, no provider, or no lockout listener.

A store, transport, provider or hasher of your own publishes none of the markers these checks read, so it is not judged.

The in-process pieces are refused because a second instance cannot see them: a limiter budget is granted once per instance, and a revocation, OAuth state or passkey challenge used on one instance stays live on the others. Redis and Valkey has the shared versions.

Reporting, scope and a deployment checklist are in SECURITY.md.