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 intodata/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_FROMand eitherRESEND_API_KEYorSMTP_*.APP_BASE_DOMAIN(e.g.learn.example.com).RATE_LIMIT_DRIVER=upstashplusUPSTASH_REDIS_REST_URLandUPSTASH_REDIS_REST_TOKEN.- Optional observability:
POSTHOG_PROJECT_KEY,NEXT_PUBLIC_POSTHOG_KEY(and optionalPOSTHOG_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. generateStaticParamsincludes published content only.- The CMS revalidation webhook (
POST /api/cms/revalidate) callsrevalidateTag('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:
- CMS project (
open-learning-hub/olh-cms, rootapps/cms) deploys first. - LMS project (
open-learning-hub/olh-lms, rootapps/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>
- 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.
- Hook env key used operationally:
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:
npm run docs:sync— mirrors rules and Markdown intodocs/zensical/docs-source/.npm run docs:build— runs Zensical and emitsdocs/zensical/site/.npm run docs:publish— runsdocs:buildthen copiessite/topublic/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:
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.xmlandGET /robots.txt— per-host; fall back to staticpublic/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. | [ ] |
| 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.