Run it
Maintenance windows
Generated from MAINTENANCE.md in the AstroBaaS repository. The repository is the source of truth; this page is a copy of it.
Two different outages need two different answers, and only one of them can be served by the application.
| Process | Served by | Covers | |
|---|---|---|---|
| In-app | running | AstroBaaS | migrations, plugin installs, planned windows |
| Static | stopped | your reverse proxy | the redeploy itself |
An in-app maintenance page cannot cover a redeploy, because during a redeploy there is no app. Shipping only the first would be a comforting thing to have and useless in the minute it was bought for.
1. In-app
The emergency switch
MAINTENANCE_MODE=1
MAINTENANCE_MESSAGE="Upgrading the shop — back shortly." # optional
MAINTENANCE_UNTIL=2026-08-25T18:00:00Z # optional
An environment variable, deliberately, because the commonest reason to need this page is that the database is migrating, busy, or unreachable — and a flag stored in the database would be unreadable exactly when it is needed. The check runs before anything touches storage or loads a plugin.
The scheduled window
Admin → Settings → Maintenance mode. A checkbox, a message, and an optional “expected back”. Use this for planned work; it needs a healthy database, which a planned window has by definition.
The difference that matters: during a scheduled window staff still see the real site, so you can check it before reopening. The emergency switch cannot offer that — it answers before any session is read, which is the whole point.
What visitors get
503 Service Unavailable, with Retry-After and Cache-Control: no-store.
Not 200. A success status tells a crawler that this is your content now;
sites have lost rankings to a maintenance notice served with 200. 503 says
“temporarily unavailable, come back”, which is both true and the thing search
engines are built to handle. API callers get the same status as JSON with
code: "MAINTENANCE", so a headless storefront does not have to parse HTML to
find out.
What stays open, and why each one has to
| Path | Because |
|---|---|
/healthz, /readyz, /metrics | a host that cannot health-check the process pulls it out of rotation, and your holding page becomes a connection error |
/admin | an operator locked out of the admin cannot turn maintenance off |
/login, /api/auth/* | logging in is how you reach the admin |
/_astro/, /favicon* | the admin behind the page is a real application |
2. Static, for the redeploy itself
Generate the page from the same source as the in-app one, so a visitor sees the same screen mid-deploy as mid-migration:
npm run build:maintenance-page
It writes public/maintenance.html — self-contained, no CSS file, no fonts, no
JavaScript, because it has to render when nothing else is available.
nginx
location / {
proxy_pass http://127.0.0.1:4321;
proxy_intercept_errors on;
error_page 502 503 504 = @maintenance;
}
location @maintenance {
root /srv/astrobaas/public;
rewrite ^ /maintenance.html break;
add_header Retry-After 120 always;
add_header Cache-Control "no-store" always;
return 503;
}
return 503 is doing real work here — without it nginx would serve the holding
page with a 200 and undo the whole point.
Caddy
handle_errors {
@down expression {err.status_code} in [502, 503, 504]
handle @down {
root * /srv/astrobaas/public
rewrite * /maintenance.html
header Retry-After 120
header Cache-Control "no-store"
file_server
}
}
A deploy with no visible downtime at all
The page above is the fallback. If you would rather visitors saw nothing:
MAINTENANCE_MODE=1on the old process, so it stops serving writes.- Deploy alongside, let the new process boot and pass
/readyz. - Switch the proxy over, stop the old one.
AstroBaaS runs migrations at boot, so the new process is the one that migrates — which is why step 1 matters: two processes writing through a schema change is the one thing this ordering avoids.