Skip to content

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:

{ "status": "ok"|"degraded", "db": "connected"|"error", "timestamp": "..." }
  • DB check: SELECT 1 via Kysely's sql tag
  • Returns 200 when healthy, 503 when degraded
  • Excluded from auth (public endpoint)
  • force-dynamic to prevent caching

Acceptance criteria

  • LMS: GET /api/health created
  • CMS: GET /api/health enhanced (previously returned static ok without DB check)
  • Returns 503 + "degraded" when DB is unreachable
  • No auth required (LMS proxy.ts matcher excludes api; CMS tenantExemptApiPatterns includes /api/health)
  • export const dynamic = "force-dynamic" prevents ISR caching
  • export const runtime = "nodejs" documents the DB driver dependency
  • Response includes Cache-Control: no-store so 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/health exempt from rate limiting (not throttled by the /api/ catch-all)
  • Co-located route.test.ts in both apps covering 200 connected, 503 error/timeout, no-store, and logging

Implemented / hardening pass (2026-07-09)

Endpoints

Both handlers:

  1. Pin runtime = "nodejs" and dynamic = "force-dynamic".
  2. Race sql\SELECT 1`.execute(db)` against a 2500ms timeout.
  3. Return { status, db, timestamp } with HTTP 200 (ok / connected) or 503 (degraded / error).
  4. Set Cache-Control: no-store on every response.
  5. 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/health in tenantExemptApiPatterns.
  • CMS: apps/cms/src/lib/api/rate-limit.ts returns null for /api/health so 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_SHA is available at runtime on Vercel, so a version field 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.