CogniPrep now has a maintenance mode. The whole switch is this:
// lib/maintenance.ts
export const MAINTENANCE: boolean = false;
export const MAINTENANCE_MESSAGE =
'CogniPrep is down for maintenance. Please try again in a little while.';
Turning it off again was a one line diff: one insertion, one deletion, one file.
A constant rather than an environment variable or a flag service is a deliberate
trade. Flipping it needs a deploy either way, because the value is read in the
proxy that runs in front of every request, and a flag I have to fetch is a flag
that can be unreachable at exactly the moment I need it. The thing you reach for
when the database is having a bad day should not itself depend on a network call.
The interesting part was not the flag. It was the four different answers a
request can get while it is on.
Four categories
function maintenanceResponse(request: NextRequest, isProtectedRoute: boolean) {
const pathname = request.nextUrl.pathname;
const isApi = hasPrefix(pathname, ['/api', '/webhook']);
const isAppPage =
isProtectedRoute || hasPrefix(pathname, MAINTENANCE_PREFIXES) || isGameRunner(pathname);
const isRead = request.method === 'GET' || request.method === 'HEAD';
if (isApi || (isAppPage && !isRead)) {
return NextResponse.json(
{ error: MAINTENANCE_MESSAGE },
{ status: 503, headers: { 'Retry-After': MAINTENANCE_RETRY_AFTER } }
);
}
if (isAppPage) {
return NextResponse.rewrite(new URL('/maintenance', request.url));
}
return setCountryCookie(NextResponse.next(), request);
}
API routes and webhooks answer 503 with Retry-After: 600. This is the one
category where the status code is doing real work rather than being polite.
Stripe and Resend both retry a webhook on a 5xx, so an event that lands during a
maintenance window is not lost, it is redelivered. A 200 with an error body
would have thrown those events away, and a 404 would have told the sender to
stop trying.
Pages that need a session or the database are rewritten to /maintenance.
Rewritten, not redirected, so the URL in the address bar stays where the user
was. When maintenance ends, a reload lands them back on the page they wanted
instead of somewhere generic.
The sign in, sign up and password pages are served as normal. They swap
their own forms for the maintenance message. That looks like duplication of the
/maintenance page until you notice why: their server actions still have to be
reachable, because someone may have had the sign in form open since before the
flag went on. Letting the page render means the action behind it can answer and
refuse with the same sentence, rather than the browser posting into a void.
Everything else is served as normal, which is the entire marketing side of
the site, and still publishes the country cookie that the price formatting
depends on.
The method check is the whole trick
isAppPage && !isRead is five tokens that handle the nastiest case in the
feature. Someone has a dashboard page open in a tab from an hour ago. The flag
goes on. They click a button that fires a server action, which is a POST to the
page's own URL. If the proxy rewrote that POST to /maintenance, the action
would get a 200 and an HTML document where it expected a server action response.
It gets the 503 instead.
Two paths with the same shape and opposite answers
function isGameRunner(pathname: string): boolean {
const [, section, slug] = pathname.split('/');
return section === 'games' && Boolean(slug) && !isAssessmentProvider(slug);
}
/games/aon is a static marketing page about a test provider. /games/<a game is the thing a signed in user plays. Same two segment shape, and one has to
id>
stay up while the other goes down. The discriminator is that provider slugs are
exactly the static folders under app/games, so anything else in that position
is a game. No second list to keep in sync.
Prefix matching is exact or followed by a slash:
function hasPrefix(pathname: string, prefixes: string[]): boolean {
return prefixes.some((p) => pathname === p || pathname.startsWith(`${p}/`));
}
A plain startsWith would have taken down /administrator and /authors
along with /admin and /auth. There are tests for exactly those two near
misses.
Bots get the same answer as people
In normal operation the proxy lets known crawlers skip session and device
handling entirely. In maintenance mode that branch never runs, because the flag
is settled before the bot check, so a crawler asking for an app page is
rewritten to /maintenance like everyone else. That page carries
robots: { index: false, follow: false }, which is what you want a crawler to
take away from a temporary outage.
The ordering is the point:
if (MAINTENANCE) {
return maintenanceResponse(request, isProtectedRoute);
}
That sits above the bot check and above the session refresh, so no request
reaches the auth provider or the database while the flag is on. The feature is
worth nothing if the middleware still makes the call it was meant to prevent.
What the tests pin
Two files, both table driven. Twelve app paths that must rewrite, six API routes
that must answer 503, sixteen public paths that must be served untouched, plus a
server action POST to /login that must be let through and a server action POST
to an app page that must not be. Every one of them also asserts that the
Supabase session refresh was never called, because "it returned the right status
code" and "it did not touch the database" are two different claims and only one
of them is the feature.
The flag ships as false, so the test file mocks it to true. The existing
proxy test file runs the same middleware with the real value, which is what pins
the other half: with the flag off, nothing changes.
See it
The flag is off right now, so what you can check live is the split it depends
on. In a private window:
- cogniprep.app/games/aon answers 200 with no session. It is a static marketing page, and it is the category that stays up.
-
cogniprep.app/dashboard in that same window
lands you on
/login. That is the category that gets rewritten. -
cogniprep.app/pricing is in the public branch
too. Open the console and read
document.cookie: thecp_countryentry is the one thing the maintenance path still bothers to set, because prices are formatted from it before anything is painted.
Top comments (0)