Skip to content

Deployment

Audience: Operators deploying the LMS to production.

Scope: Build, environment, ISR/cache configuration, the documentation pipeline, and the launch checklist. Authoritative security guidance is security-and-rate-limiting.md.

Build & Run

npm install
npm run build      # Next.js production build (no Turbopack for build)
npm run start      # Next.js production server (port 3001 by default)

npm run dev is for local development only. Production must run next start against the build output.

Build Dependency On CMS

The LMS build prerenders CMS-backed routes, so the CMS must be reachable during next build.

  • Local/CI: use the integrated root command from the monorepo root:
    • npm run build:integrated
  • This workflow starts CMS on port 3099 (not dev :3000), seeds demo content into data/integrated-build, disables CMS rate limiting, and builds LMS against that isolated CMS instance.
  • For plain npm run build, CMS-backed prerender can degrade to fallback behavior if CMS is unavailable.

Required Environment Variables

See security-and-rate-limiting.md for the full table. Verify each variable in production:

  • AUTH_SECRET, AUTH_URL, AUTH_TRUST_HOST.
  • DATABASE_URL (Postgres).
  • CMS_API_URL, CMS_PROJECT_UUID, CMS_API_UUID, CMS_WEBHOOK_SECRET.
  • EMAIL_FROM and either RESEND_API_KEY or SMTP_*.
  • APP_BASE_DOMAIN (e.g. learn.example.com).
  • RATE_LIMIT_DRIVER=upstash plus UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.
  • Optional observability: POSTHOG_PROJECT_KEY, NEXT_PUBLIC_POSTHOG_KEY (and optional POSTHOG_HOST / NEXT_PUBLIC_POSTHOG_HOST, default US Cloud). See Observability & Product Analytics.

Email uses Resend first, then SMTP; production has no console fallback. Configure and verify the selected provider using Email Delivery. A successful account-recovery UI response is enumeration-safe and does not confirm provider acceptance.

Server-side variables MUST NOT use the NEXT_PUBLIC_ prefix.

ISR & Caching

  • All public/learner pages set export const revalidate = 60.
  • generateStaticParams includes published content only.
  • The CMS revalidation webhook (POST /api/cms/revalidate) calls revalidateTag('course:<slug>'), revalidateTag('cms:page-hierarchy'), etc. Configure the CMS to fire this webhook on publish, unpublish, and content correction events.
  • LMS-owned tags (progress:<userId>) are revalidated by server actions, not by the CMS.

Vercel Topology

Deploy LMS and CMS as separate Vercel projects in the same monorepo:

  1. CMS project (open-learning-hub/olh-cms, root apps/cms) deploys first.
  2. LMS project (open-learning-hub/olh-lms, root apps/lms) uses the live CMS URL at build time via:
    • CMS_API_URL=https://<cms-domain>/api/v1/
    • CMS_PROJECT_UUID=<site-config-project-uuid>
    • CMS_API_UUID=<cms-api-key>
  3. Configure a CMS -> LMS Deploy Hook trigger after successful CMS deploys so LMS static output is refreshed when CMS content structures change.
    • Hook env key used operationally: LMS_DEPLOY_HOOK_URL.

POST /api/cms/revalidate remains the fast path for tag-based ISR updates between full LMS rebuilds.

Database

Production uses PostgreSQL. Local development uses SQLite for speed; do not ship SQLite to production.

The LMS uses separate Neon databases, one per Vercel environment, so every environment owns its DATABASE_URL and never writes to another environment's data.

# Apply pending migrations (runs in the Vercel build step on every environment)
npm run db:migrate:deploy

# System seed: idempotent bootstrap super_admin (runs on every deploy)
npm run db:seed:system

# Demo/sample data — local dev and e2e only; never run on deploy
npm run db:seed

scripts/vercel-db.mjs runs on all Vercel environments (production and non-production). It first runs db:migrate:deploy, then db:seed:system. Both steps are idempotent, so repeated or concurrent builds are safe. Prerequisite: each environment must point at its own DATABASE_URL (already true with one Neon database per environment). Canonical root script guidance lives in Root Scripts Reference.

The system seed creates only an env-driven bootstrap super admin (tenant_id = null); set SEED_SUPERADMIN_EMAIL and SEED_SUPERADMIN_PASSWORD in the Vercel project env. When unset, the super-admin step logs a warning and no-ops. The demo seed (db:seed) — sample tenants, personas, course assignment — is never run on deploy.

Migrations run portably against both engines. Helper utilities live in src/db/migrations/_helpers.ts.

Documentation Pipeline

The published documentation site is part of the LMS production bundle. From apps/lms:

  1. npm run docs:sync — mirrors rules and Markdown into docs/zensical/docs-source/.
  2. npm run docs:build — runs Zensical and emits docs/zensical/site/.
  3. npm run docs:publish — runs docs:build then copies site/ to public/docs/.

The predev script runs docs:publish for local npm run dev only. Production builds must run docs:publish (or an equivalent docs step) in the deployment pipeline before next build so /docs/ is present in the bundled static assets.

On Vercel, scripts/vercel-docs.mjs runs as the first step of the LMS build command:

node ../../scripts/vercel-docs.mjs && node ../../scripts/vercel-db.mjs && npm run build

The script ensures uv is available, then runs npm run docs:publish so the public/docs/ bundle exists before the Next.js production build starts.

Health Checks

  • GET /api/test/health — basic liveness check (when present).
  • GET /openapi.yaml — OpenAPI surface; fails if Zod schemas are out of date.
  • GET /sitemap.xml and GET /robots.txt — per-host; fall back to static public/fallback/* if the CMS is down.

Launch Checklist

Area Item Status
Pre-launch All environment variables set; AUTH_SECRET rotated. [ ]
Pre-launch Production database backed up before first deploy. [ ]
Database Migrations applied; seed data only on staging. [ ]
Database Indexes verified per development/database-schema.md. [ ]
Auth Sign-in / sign-up / forgot / reset / magic-link e2e green. [ ]
Email External provider configured; sender verified; transactional smoke tests green. [ ]
Auth Suspended-tenant 503 verified. [ ]
API OpenAPI generation reproducible; /openapi.yaml available. [ ]
API CMS revalidation webhook configured with CMS_WEBHOOK_SECRET. [ ]
Frontend All locales render the home and sign-in pages. [ ]
Frontend Theme smoke (light / dark / system) on top-level routes. [ ]
Performance Lighthouse pass on home, course landing, learn dashboard. [ ]
Monitoring PostHog receives errors from server and client; structured logger redaction verified. [ ]
Monitoring Alerts on auth failure spikes, rate-limit exhaustion, payment-webhook failures, CMS 5xx. [ ]
Post-launch Audit log appears for impersonation, role changes, tenant transitions, enrollment changes. [ ]
Post-launch Per-tenant well-known files (favicon, manifest, robots, sitemap, OG/Twitter) verified. [ ]

Open launch-checklist items are tracked in roadmap.md under Implementation Plan Follow-Ups.