Skip to content

feat(prod): /api/health + boot-time env validation#22

Open
salishforge wants to merge 1 commit into
masterfrom
feat/health-and-env-validation
Open

feat(prod): /api/health + boot-time env validation#22
salishforge wants to merge 1 commit into
masterfrom
feat/health-and-env-validation

Conversation

@salishforge

Copy link
Copy Markdown
Owner

Summary

First slice of production-readiness work toward the beta launch. Two thin additions, no behaviour change to existing routes:

  • src/lib/env.ts — pure environment validator. Two severity bands:

    • Required at boot (DATABASE_URL, NEXT_PUBLIC_APP_URL, explicit AUTH_MODE, Supabase keys when AUTH_MODE=supabase). In production: missing → throw. Catches the audit-found case where NEXT_PUBLIC_APP_URL silently falls back to http://localhost:3000 and Stripe success/cancel redirects route real prod traffic to dev.
    • Feature-critical (Stripe, Resend, Novu, eBay, PriceCharting, Anthropic, cron). Missing → warning only; the feature's own client decides whether to throw or no-op when actually used. Health endpoint surfaces these so operators can tell which integrations are inert.

    Also rejects production NEXT_PUBLIC_APP_URL values containing localhost / 127.0.0.1 or missing http(s):// — copy-pasted dev values that pass non-empty presence checks but break in prod.

  • src/instrumentation.ts — Next 16 instrumentation hook. Calls assertEnv() once at server boot. Node-runtime-only. In production an aggregated error refuses to start the server; in dev warnings log but boot continues.

  • src/app/api/health/route.tsGET /api/health. No auth (LBs and uptime probes need to reach it). SELECT 1 against the DB → 200/ok on success, 503/down on failure. Re-runs validateEnv() each call so post-restart env rotations surface without a redeploy. Never echoes secret values — only category warnings like "stripe: not configured".

  • .env.example — three audit-found gaps documented:

    • NEXT_PUBLIC_APP_URL — production-critical
    • WONDERS_DECK_PLATFORM_API_URL — used by platform/client.ts for engine-metric sync
    • WONDERSTRADINGPOST_ANON_KEY — public anon key surfaced for upstream-rotation visibility

Tests

  • tests/env/env.test.ts — 18 cases: production posture for each required var, dev posture (warnings only), AUTH_MODE matrix, feature-warning roll-ups, empty/whitespace-as-missing semantics.
  • tests/health/health.test.ts — 3 cases: 200 happy path, 503 on DB ping failure, env-warnings block shape.

Verification

  • npx tsc --noEmit clean
  • npm run lint — 0 errors, 0 warnings on touched files
  • npx vitest run → 44/44 across env, health, attribution, packs (21 new + 23 pre-existing)
  • DATABASE_URL=… npm run build → exit 0, 39 routes compiled

Context

Per the production-readiness assessment in this session, this is one chunk of P0 work. Other P0 items (rebase + land #21, security headers, admin-bootstrap script, Dockerfile, prod migration runner) are still ahead.


Generated by Claude Code

First slice of production-readiness work. Two thin additions, no
behavior change to existing routes:

src/lib/env.ts — pure validator
  Pass in a Record<string, string|undefined>, get back a structured
  result with errors and warnings. Two severity bands:
  - Required at boot (DATABASE_URL, NEXT_PUBLIC_APP_URL,
    AUTH_MODE-explicit, Supabase keys when AUTH_MODE=supabase). In
    production, missing → throw. Catches the audit-found case where
    NEXT_PUBLIC_APP_URL silently falls back to localhost:3000 and
    Stripe success/cancel redirects route real prod traffic to dev.
  - Feature-critical (Stripe, Resend, Novu, eBay, PriceCharting,
    Anthropic, cron). Missing → warning only; the feature's own
    client decides whether to throw or no-op when actually used.
  Also rejects production NEXT_PUBLIC_APP_URL values containing
  localhost / 127.0.0.1 or missing http(s):// — covers copy-pasted
  dev values that pass non-empty presence checks.

src/instrumentation.ts — Next 16 instrumentation hook
  Calls assertEnv() once at server boot. Node-runtime-only (the
  validator's surface is server-side). In production, an aggregated
  error refuses to start the server; in dev, warnings log but boot
  continues.

src/app/api/health/route.ts — /api/health
  GET-only, no auth (LBs and uptime probes need to reach it):
  - SELECT 1 against the DB; 200/ok on success, 503/down on failure.
  - Re-runs validateEnv() each call so post-restart env rotations
    surface without a redeploy; never echoes secret values.
  - Reports uptimeSeconds + ISO timestamp.

.env.example — three audit-found gaps
  - NEXT_PUBLIC_APP_URL: documented as production-critical.
  - WONDERS_DECK_PLATFORM_API_URL: used by platform/client.ts.
  - WONDERSTRADINGPOST_ANON_KEY: public anon key surfaced for
    upstream-rotation visibility.

Tests
  - tests/env/env.test.ts (18 cases): production posture for each
    required var, dev posture (warnings only), AUTH_MODE matrix,
    feature-warning roll-ups, empty/whitespace-as-missing.
  - tests/health/health.test.ts (3 cases): 200 happy path, 503 on
    DB ping failure, env-warnings block shape.

Verification
  - npx tsc --noEmit clean
  - npm run lint clean on touched files (0 errors, 0 warnings)
  - npx vitest run tests/env tests/health tests/attribution tests/packs
    → 44/44 (21 new + 23 existing)
  - DATABASE_URL=… npm run build → exit 0, 39 routes compiled
@supabase

supabase Bot commented Jun 18, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project amxgqxjqqofqitjxfytq because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants