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:
| Flag | What it blocks |
|---|---|
maintenance_landing | Public marketing site (/) and public signup links |
maintenance_companies | Employer signin + company dashboards |
maintenance_jobseekers | Jobseeker 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
- Sign in as a super admin on the affected environment.
- Navigate to
/admin/maintenance. - Tick any combination of Landing / Companies / Jobseekers.
- (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.
- Fill in Reason (short) and/or Message (long) so users see something human on the lock screen. Empty fields are hidden.
- Optionally provide a Support contact (email or URL) and Status page URL.
- 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 clearsmaintenance_ends_atin a single batch. - Or: let
maintenance_ends_atexpire. The backend forces all three locks tofalseonceserverNow >= 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/maintenancewhile locks are on still sees the page normally. - Signin with
?bypass=1— the/auth/signinpage reads abypass=1query 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).
serverNowis 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|deletedevents; if the event payload'skeystarts withmaintenance_(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 withlandingLocked = true→ seeMaintenanceScreen, not the marketing page. - Visit
https://dev.aiqlick.com/auth/signinwithcompaniesLocked = true→ blocked; add?bypass=1→ signin form appears. - Sign in as a super admin → bypass applies, all dashboards work normally.