Offline on purpose
Maintenance Mode
Take an app offline for a deployment, migration, or incident: every request answers
503 Service Unavailable until you turn it off, while the team can
still reach the site through a bypass secret.
Turning it on and off
npm run down -- --with-secret --retry 120 --message "Upgrading the database" npm run maintenance:status npm run up # or directly uv run python -m casp.maintenance down|up|status
| Option | Effect |
|---|---|
| --message TEXT | Text shown to visitors and returned in API/RPC errors |
| --retry SECONDS | Retry-After header (default 60) |
| --secret VALUE / --with-secret | Enable a bypass URL; --with-secret generates one and prints it |
| --allow /path | Keep a URL subtree available (repeatable) |
State lives in .casp/maintenance.json with only a SHA-256 hash of the secret;
npm run dev recreates .casp/, which clears it. A state file that
exists but cannot be parsed keeps the app down with default settings, because someone intended maintenance.
Whole deployments
For several containers, use environment variables. A state file takes precedence over the environment.
MAINTENANCE_MODE=true
Turn maintenance on.
MAINTENANCE_SECRET
Bypass secret.
MAINTENANCE_RETRY_AFTER
Retry-After seconds.
MAINTENANCE_MESSAGE
Visitor message.
What visitors get
-
Browsers:
src/app/maintenance.pywhen present, otherwise a built-in escaped page. -
pp.rpccalls:errorwith the message and status 503. -
API clients:
application/problem+json. -
WebSockets: closed with code 1013 (try again later);
pp.socketkeeps retrying with backoff and reconnects once the app is back.
Blocked responses carry Retry-After, no-store, the request id,
and security headers. public/ files, /health,
/ready, and --allow subtrees keep working — probes stay up on
purpose so orchestrators do not restart a healthy process.
The bypass secret
With a secret configured, visiting /<secret> sets an HTTP-only
casp_maintenance_bypass cookie and redirects to /. That browser
uses the site normally so the team can verify a deployment before turning maintenance off. The cookie is
derived from the secret's hash, so changing the secret invalidates it.
Treat the secret like a password: share it privately and use a new one each time.
A custom maintenance page
# src/app/maintenance.py
from casp.component_decorator import html
from casp.layout import Metadata
metadata = Metadata(title="Maintenance", extra={"robots": "noindex"})
def page(maintenance):
return html(r"""
<main class="container py-16 text-center">
<h1 class="text-3xl font-semibold">We'll be right back</h1>
<p class="mt-4 text-gray-600">{{ maintenance.message }}</p>
</main>
""", maintenance=maintenance)
It renders through the root layouts but outside the session and auth layers, so a broken database or session store cannot block the 503. A layout that reads the session or queries the database fails here and the built-in page is served instead — keep the maintenance page and its layouts free of per-user data.