Skip to main content

Maintenance Mode Runbook

Maintenance mode lets a super admin temporarily close parts of AIQLick — the public landing page, the companies side, and/or the jobseeker side — without a code deploy. Super admins always bypass the lock. This runbook explains what the feature does, when to use it, and how the pieces fit together.

Overview

Three independent surfaces can be locked. Each has its own flag:

FlagWhat it blocks
maintenance_landingPublic marketing site (/) and public signup links
maintenance_companiesEmployer signin + company dashboards
maintenance_jobseekersJobseeker signin + jobseeker dashboards

Any combination is valid. All three can be on at once; all three can be off; you can lock just employers while jobseekers keep working, etc.

When to use it

Good fits:

  • Planned maintenance windows — deploying a breaking schema change, rotating a secret the app reads at boot, running a one-off backfill script that would corrupt live reads.
  • Incidents — a data-integrity bug is actively writing bad rows and you need to stop traffic before more damage accumulates.
  • Billing or auth provider outages — Stripe webhook endpoint is down, or the IdP is failing, and it's better to show a maintenance screen than a cryptic 500.

Bad fits:

  • Staged rollouts — use a feature flag with gradual percentage rollout instead.
  • Per-company or per-user gating — this flag is global.
  • Anything longer than a few hours — locking jobseekers for a full business day is a customer-trust event, not a maintenance window. Communicate first.

How to enable it

  1. Sign in as a super admin on the affected environment.
  2. Navigate to /admin/maintenance.
  3. Tick any combination of Landing / Companies / Jobseekers.
  4. (Recommended) set Auto-reactivation — a datetime in the future when the backend automatically unlocks everything even if nobody is around to flip the switches back.
  5. Fill in Reason (short) and/or Message (long) so users see something human on the lock screen. Empty fields are hidden.
  6. Optionally provide a Support contact (email or URL) and Status page URL.
  7. Click Save.

On save, the admin UI writes each changed flag via adminSetFeatureFlag. Within one 60-second poll cycle, every anonymous visitor sees the lock; the cache-bust listener means active admin users see the change in under a second.

How to disable it

  • Click "End maintenance now" in /admin/maintenance — flips all three locks off and clears maintenance_ends_at in a single batch.
  • Or: let maintenance_ends_at expire. The backend forces all three locks to false once serverNow >= endsAt, so even a stale admin who forgot to turn it off won't leave the site locked forever.

Who bypasses the lock

  • Super admins (user.isSuperAdmin) — always bypass. The check is client-side; the backend resolver stays public and dumb. That means a super admin hitting /admin/maintenance while locks are on still sees the page normally.
  • Signin with ?bypass=1 — the /auth/signin page reads a bypass=1 query string and skips the lock. Use this when you need to sign in as a super admin while companiesLocked or jobseekersLocked is on. It's an escape hatch, not a backdoor: non-superadmins still get rejected after login by the role-aware authGuard.

Server-side semantics (what the backend actually does)

The publicSiteStatus GraphQL query is public and unauthenticated — no JwtAuthGuard. Anyone on the internet can call it. The response is tiny (9 fields) and safe to expose.

Flag coercion:

  • Booleans: only the exact string "true"true. Anything else (including "1", "TRUE", missing key) → false.
  • Strings: empty string ""null.

Expiry enforcement is authoritative:

if maintenance_ends_at parses to a valid date AND now >= endsAt:
landingLocked = companiesLocked = jobseekersLocked = false

Even if the stored values are all "true", the query returns them as false once the schedule expires. endsAt itself is still returned so the frontend can display the original value.

Unparseable maintenance_ends_at (e.g. a garbage string) is treated as "no schedule" — locks remain as stored.

Caching:

  • 10-second in-memory TTL on the flag projection (the database read).
  • serverNow is never cached — recomputed on every call, so the frontend can use it as a reliable anchor for countdown displays.
  • The service listens for admin.featureFlag.created|updated|deleted events; if the event payload's key starts with maintenance_ (or is missing, as a conservative default), the cache is busted immediately. Admin toggles propagate well inside the 10s window.

Verification

After enabling maintenance on dev:

# Anonymous — no Authorization header
curl -s -X POST https://api-dev.aiqlick.com/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ publicSiteStatus { landingLocked companiesLocked jobseekersLocked endsAt serverNow } }"}'

Expected: 200 with the current flag state. No token required.

End-to-end behavior:

  • Visit https://dev.aiqlick.com/ in a private window with landingLocked = true → see MaintenanceScreen, not the marketing page.
  • Visit https://dev.aiqlick.com/auth/signin with companiesLocked = true → blocked; add ?bypass=1 → signin form appears.
  • Sign in as a super admin → bypass applies, all dashboards work normally.