Operations facet
WIPMaintenance mode + read-only mode toggles. Drain traffic before a migration; freeze writes during incident response.
What it gives you
auth.operations is the runtime-control facet. Two toggles:
- Maintenance mode - every request raises
AUTH/MAINTENANCE(503). Use during planned downtime. - Read-only mode - sign-ins still work, but every write
(
identities.create,passwords.changePassword, etc.) raisesAUTH/READONLY_MODE. Use during incident response or while migrating a database.
await auth.operations.enterMaintenance({ reason: 'database-migration' })
await auth.operations.leaveMaintenance()
await auth.operations.enterReadonly({ reason: 'incident-2026-05-27' })
await auth.operations.leaveReadonly()
const status = await auth.operations.status()
// -> { maintenance: boolean, readonly: boolean, since?: Date, reason?: string }
Behaviour during maintenance
Every call to resolveSession, flows.signIn, flows.signOut, every
provider's begin/complete, and every facet mutation raises:
throw new AuthError('AUTH/MAINTENANCE', {
since: '2026-05-27T10:00:00Z',
reason: 'database-migration',
})
Clients should display a maintenance banner and pause polling. The
return-to-normal happens automatically when leaveMaintenance() is
called - no restart required.
Behaviour during read-only mode
Reads (identities.findById, orgs.listForIdentity,
apiKeys.list, resolveSession, flows.signIn) work normally.
Writes raise:
throw new AuthError('AUTH/READONLY_MODE', {
since: '2026-05-27T10:00:00Z',
reason: 'incident-2026-05-27',
})
Use read-only mode when you need to keep users signed in (no disruption to existing sessions) but want to freeze the data layer. Common during database master failover or schema migrations.
Cross-replica sync
In a multi-replica deployment, the toggle must propagate to every replica. Use a Redis-backed state:
import Redis from 'ioredis'
import { RedisOperationsStore } from '@gentleduck/auth/adapters/redis'
const redis = new Redis(process.env.REDIS_URL!)
new AuthEngine({
// ...
operations: {
store: new RedisOperationsStore(redis, { key: 'auth:ops' }),
},
})
Now enterMaintenance writes to Redis; every replica reads on each
incoming request. Use a short read TTL (5-10s) so the propagation lag
is bounded.
Bypass keys
For "the system is in maintenance mode but admins still need to fix it" workflows, support a bypass key:
new AuthEngine({
// ...
operations: {
bypassKey: process.env.OPS_BYPASS_KEY,
},
})
// Requests carrying X-Ops-Bypass: <key> are exempt from maintenance.
Treat the bypass key as a credential: rotate it, log every use, and never share it in a chat room. Production deployments should also gate by source IP (only allow from your VPC bastion).
Health check + readiness
operations.status() can drive your Kubernetes readiness probe:
app.get('/healthz', async (req, res) => {
const ops = await auth.operations.status()
if (ops.maintenance) {
res.status(503).json({ status: 'maintenance', since: ops.since })
return
}
res.status(200).json({ status: 'ok' })
})
In maintenance mode, the probe returns 503; the load balancer routes traffic away. When you leave maintenance, the probe returns 200 and traffic resumes - no restart needed.
Events emitted
| Event | Payload |
|---|---|
operations.maintenance-entered | { reason, actorId? } |
operations.maintenance-left | { durationMs, actorId? } |
operations.readonly-entered | { reason, actorId? } |
operations.readonly-left | { durationMs, actorId? } |
Wire these to your incident management / PagerDuty for a "system is in maintenance" alert.