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.

This is a website maintenance gate, not a backend shutdown

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

  1. Sign in as a platform administrator.
  2. Open /admin-dashboard/settings in the current locale.
  3. Find Maintenance mode and change the switch.
  4. 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 maintenance status is cached

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

RequestMaintenance behavior
Localized website pageTemporary 307 redirect to /[locale]/maintenance
/docs and documentation pagesRedirect to the maintenance page using the locale cookie or negotiated browser language
/[locale]/maintenanceAlways renders; marked noindex, nofollow
/[locale]/admin-dashboard/**Remains reachable and keeps its normal authentication/administrator checks
Login, callback, and password-recovery pagesRemain reachable so an administrator cannot be locked out
Non-GET request to a page route503 JSON response with code MAINTENANCE_MODE and Retry-After: 300
/api/**, webhooks, jobs, and /api/healthContinue through their existing security and availability controls
/_next/**, public assets, /offlineRemain 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

  1. Apply the maintenance migration and its matching supabase/schema.sql state.
  2. Deploy the application code.
  3. Open each /[locale]/maintenance page directly and review the translations.
  4. Enable the flag in a staging environment and verify a public page and /docs redirect.
  5. Verify the admin dashboard, login/callback flow, /api/health, webhooks, and jobs remain reachable.
  6. 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.