Skip to main content

Operations facet

WIP

Maintenance 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.) raises AUTH/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

EventPayload
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.