maintenance.ts65 lines · main
1import type { MiddlewareHandler } from 'hono';
2
3import { getMaintenanceState } from '../services/platform-settings.js';
4
5/**
6 * Platform-wide maintenance gate. Gates on the EFFECTIVE maintenance
7 * state (manual `maintenanceMode` override OR an active scheduled
8 * `maintenanceWindow`) — see getMaintenanceState(). When active, every
9 * request returns 503 except a small whitelist:
10 * - /health, /ready (so the operator + monitoring still see liveness)
11 * - /v1/auth/* (Better Auth — admin must be able to sign in)
12 * - /v1/me, /v1/me/* (the signed-in admin needs /me + /me/step-up)
13 * - /v1/admin/* (admin actions, including the toggle to flip
14 * maintenance mode back off)
15 * - /v1/status/* (public status feed — incidents + maintenance
16 * state — MUST stay reachable so the marketing
17 * site can render the maintenance page)
18 *
19 * The flag lives in platform_settings (DB-backed, 60s cached) so an
20 * admin can flip it from the dashboard without an env change or
21 * deploy. Cache TTL means the change takes effect within ~60s on peer
22 * instances and immediately on the writer.
23 *
24 * Mounted at app level on /v1/* AFTER attachSession + before any
25 * project-scoped middleware, so /health (no /v1 prefix) is naturally
26 * exempt and Better Auth's /v1/auth/* responds normally for the
27 * sign-in path.
28 */
29export function maintenanceMode(): MiddlewareHandler {
30 return async (c, next) => {
31 const path = new URL(c.req.url).pathname;
32 if (isWhitelisted(path)) {
33 await next();
34 return;
35 }
36 const state = await getMaintenanceState();
37 if (!state.active) {
38 await next();
39 return;
40 }
41 return c.json(
42 {
43 code: 'maintenance_mode',
44 message:
45 'briven is in maintenance mode. critical paths (auth, /health, /ready, admin) remain available.',
46 },
47 503,
48 );
49 };
50}
51
52function isWhitelisted(path: string): boolean {
53 if (path === '/health' || path === '/ready') return true;
54 if (path.startsWith('/v1/auth/')) return true;
55 if (path === '/v1/me' || path.startsWith('/v1/me/')) return true;
56 if (path.startsWith('/v1/admin/')) return true;
57 // Public status feed (incidents + maintenance state). Kept open so the
58 // marketing site can read maintenance state to render its status page
59 // DURING maintenance — otherwise the web can't discover the window.
60 if (path.startsWith('/v1/status/')) return true;
61 // /info is the build-info probe used by the dashboard footer. Keep
62 // it open so the operator's own dashboard renders during maintenance.
63 if (path === '/info') return true;
64 return false;
65}