Health check endpoint
Problem¶
No health check endpoints existed. Vercel's built-in checks only verify function responsiveness, not database connectivity. Monitoring dashboards and external probes need a lightweight endpoint to detect degraded state.
Proposal¶
Add GET /api/health to both apps returning:
- DB check:
SELECT 1via Kysely'ssqltag - Returns 200 when healthy, 503 when degraded
- Excluded from auth (public endpoint)
force-dynamicto prevent caching
Acceptance criteria¶
- LMS:
GET /api/healthcreated - CMS:
GET /api/healthenhanced (previously returned staticokwithout DB check) - Returns 503 +
"degraded"when DB is unreachable - No auth required (LMS
proxy.tsmatcher excludesapi; CMStenantExemptApiPatternsincludes/api/health) -
export const dynamic = "force-dynamic"prevents ISR caching -
export const runtime = "nodejs"documents the DB driver dependency - Response includes
Cache-Control: no-storeso CDNs/proxies never serve a stale health result - Request-level DB timeout (~2500ms) so a stuck pool cannot hang the probe
- Structured error logging on DB failure (both apps use
@/lib/log; see.cursor/rules/091-structured-logging.mdc) - CMS
/api/healthexempt from rate limiting (not throttled by the/api/catch-all) - Co-located
route.test.tsin both apps covering 200 connected, 503 error/timeout,no-store, and logging
Implemented / hardening pass (2026-07-09)¶
Endpoints¶
Both handlers:
- Pin
runtime = "nodejs"anddynamic = "force-dynamic". - Race
sql\SELECT 1`.execute(db)` against a 2500ms timeout. - Return
{ status, db, timestamp }with HTTP 200 (ok/connected) or 503 (degraded/error). - Set
Cache-Control: no-storeon every response. - Log DB failures without crashing the health endpoint (both apps use the canonical structured logger).
Auth and rate-limit surface¶
- LMS: apps/lms/src/proxy.ts matcher already excludes
api, so health never hits auth/tenant middleware. - CMS: apps/cms/src/proxy.ts lists
/api/healthintenantExemptApiPatterns. - CMS: apps/cms/src/lib/api/rate-limit.ts returns
nullfor/api/healthso probes are not counted against the general/api/bucket (100/min/IP). Rate limiting runs before the tenant-exempt early return, so an explicit exemption is required.
Tests¶
- LMS
apps/lms/src/app/api/health/route.test.ts - CMS
apps/cms/src/app/api/health/route.test.ts - CMS rate-limit exemption coverage in
apps/cms/src/lib/api/__tests__/rate-limit.test.ts
Out of scope¶
- Git SHA / version in response — intentionally omitted for a minimal probe payload.
VERCEL_GIT_COMMIT_SHAis available at runtime on Vercel, so aversionfield is feasible later without build-time injection. - Uptime counter (not meaningful in serverless)
- Separate readiness vs liveness probes (Vercel doesn't use them)
- Checking CMS↔LMS HTTP connectivity from either health endpoint (would couple deploy order and amplify cascading failures)
Notes / decisions log¶
- 2026-07-07: Proposed. CMS currently has a skeleton health route returning static
{ status: "ok" }; plan is to align both apps with DB connectivity checks and degraded status handling. - 2026-07-09: Deferred — moved to tickets/deferred/; no active implementation planned.
- 2026-07-09: Implemented + hardening pass. Moved out of deferred/, marked done. Codified
no-store, request-level DB timeout, structured failure logging, CMS rate-limit exemption,runtime = "nodejs", and co-located tests. Corrected the stale "git SHA requires build-time injection" rationale.