Maintenance mode lets a platform administrator temporarily take the website pages offline from Admin Dashboard → Settings. Visitors are redirected to a localized maintenance screen after the shared status cache refreshes; no environment-variable change, rebuild, or redeploy is required.
The flag is disabled by default and stored in app_settings. Apply supabase/migrations/20260815133513_add_maintenance_mode.sql before using the control on an existing deployment.
API routes, Stripe webhooks, background jobs, health probes, static assets, and the offline fallback remain available by design. This keeps billing events, scheduled work, monitoring, and administrator recovery operational. Stop those systems separately when maintenance requires a complete backend outage.
Enable or disable maintenance mode
- Sign in as a platform administrator.
- Open
/admin-dashboard/settingsin the current locale. - Find Maintenance mode and change the switch.
- Review the confirmation dialog and confirm the change.
Changing this setting is a sensitive administrator action. The API requires a recent authenticated session; if the session is too old, sign in again and repeat the change. Enabling and disabling both produce an admin_logs audit row.
The Settings control displays the current 60-second cache window. The switch and audit log update as soon as the database write succeeds, but visitor routing is not immediate: enabling or disabling maintenance can continue using the previous status until the shared server cache refreshes.
The public maintenance page remains directly available at /[locale]/maintenance while the flag is off, so operators can preview every configured locale before an incident.
Request behavior
| Request | Maintenance behavior |
|---|---|
| Localized website page | Temporary 307 redirect to /[locale]/maintenance |
/docs and documentation pages | Redirect to the maintenance page using the locale cookie or negotiated browser language |
/[locale]/maintenance | Always renders; marked noindex, nofollow |
/[locale]/admin-dashboard/** | Remains reachable and keeps its normal authentication/administrator checks |
| Login, callback, and password-recovery pages | Remain reachable so an administrator cannot be locked out |
| Non-GET request to a page route | 503 JSON response with code MAINTENANCE_MODE and Retry-After: 300 |
/api/**, webhooks, jobs, and /api/health | Continue through their existing security and availability controls |
/_next/**, public assets, /offline | Remain available so the maintenance page and recovery surfaces can render |
Requests without a locale prefix first follow the normal permanent locale redirect, then enter maintenance mode. The maintenance response is no-store, carries Retry-After: 300, and is excluded from search indexing.
Security and audit model
The browser never writes app_settings directly. The control sends a strict boolean to PATCH /api/admin/settings through csrfFetch().
The request passes all of these gates before the database changes:
apiSecurity.admin()authentication and platform-admin authorization- the standard administrator rate limit
- Double Submit Cookie CSRF validation
- strict Zod validation for
{ key: 'maintenance_mode', value: boolean } - the configured recent-authentication requirement
The set_maintenance_mode(boolean) database RPC then repeats the platform-admin check using auth.uid() and profiles.is_admin. It updates the flag and inserts the admin_logs row in one transaction, so the setting cannot change without its audit record. Execute permission is revoked from anon and service_role and granted only to authenticated callers.
Public page enforcement reads only the scalar is_maintenance_mode() RPC. Anonymous and authenticated callers can execute that function, but neither role receives direct read access to the app_settings table.
Availability and performance
The proxy reads the scalar flag through a shared Next.js server cache. getMaintenanceMode() uses createServiceClient() inside a module-scope unstable_cache() reader, so requests reuse the result instead of querying Supabase for every eligible page. The cache key and 60-second revalidation window live in config/maintenance.ts.
The admin mutation deliberately does not invalidate the cached value. This reduces database traffic on the global request path at the cost of delayed activation and restoration, which the Settings UI discloses. On routes that also resolve a user, the cached maintenance lookup and session verification still run concurrently.
See Caching & Performance for the shared data-cache inventory and cache-safety rules.
If the lookup fails because Supabase or the network is unavailable, the proxy deliberately fails open and logs the maintenance/status_lookup_failed event. A database incident must not silently turn into an unrelated global maintenance outage. Use infrastructure-level routing or your hosting provider's maintenance controls when the application database itself is unavailable and traffic must fail closed.
Deployment checklist
- Apply the maintenance migration and its matching
supabase/schema.sqlstate. - Deploy the application code.
- Open each
/[locale]/maintenancepage directly and review the translations. - Enable the flag in a staging environment and verify a public page and
/docsredirect. - Verify the admin dashboard, login/callback flow,
/api/health, webhooks, and jobs remain reachable. - Disable the flag, wait for the displayed cache window, and confirm public pages are restored.
No environment variable is required. The database seed is false, so applying the migration cannot put a running deployment into maintenance mode by itself.