Skip to content

Course Configuration

Status: Authoritative for per-course LMS feature flags (not CMS project settings).

Course-level settings live in the LMS database because they control learner playback behaviour and home-page curation keyed by CMS course slug. There is no separate LMS courses table — the slug matches enrollments.course_slug and the lowercase CMS acronym.

Data model

course_settings(
  id                 text primary key,
  tenant_id          text not null references tenants(id),
  course_slug        text not null,
  feature_flags_json json not null default '{}',
  created_at         timestamptz not null,
  updated_at         timestamptz not null,
  unique (tenant_id, course_slug)
)

When no row exists for a (tenant_id, course_slug) pair, the registry supplies defaults (all flags off unless documented otherwise).

Feature flags (v1)

Flag Default Purpose
catalog.featured false Shows the course in the tenant home page Featured courses section.
catalog.listed false Makes the course visible in LMS catalog/search and available for new enrollment on tenant hosts.
entries.sequentialGating false Per-module sequential gating; within each module, earlier entries must be complete before later entries unlock. Cross-module locking is intentionally off.

Wired flags are listed in src/lib/course/feature-flag-registry.ts. Add keys only when application code reads them.

Resolution:

  • getCourseFeatures(db, { tenantId, courseSlug }) — merged catalog + stored JSON.
  • isCatalogListed(db, tenantId, courseSlug) — convenience for catalog and enrollment visibility checks.
  • isSequentialGatingEnabled(db, tenantId, courseSlug) — convenience for entry layout and gating.

Client components receive resolved booleans (e.g. gateStates on the sidebar); they never see raw JSON.

Admin management

Role Surface Capability
course_admin /admin/courses/\[slug\]/settings Flags for assigned courses only (verify: true)
tenant_admin /admin/courses/\[slug\]/settings Any course in the tenant (CMS slug must exist)
super_admin Same as other admin course pages on the host Session tenantId or host x-tenant-id on tenant subdomain

From /admin/courses, each row links Settings/admin/courses/\[slug\]/settings. The settings screen uses two tabs:

  • Visibility — course discoverability controls (catalog.listed, catalog.featured).
  • Learning experience — learner progression controls (currently entries.sequentialGating).

Tenant host required: course_settings rows are keyed by tenant_id. The apex/public host (http://lvh.me:3001 when APP_BASE_DOMAIN=lvh.me:3001) does not resolve a tenant, so super_admin on apex sees an explanatory page instead of a 404. Open the same path on a tenant subdomain (e.g. http://demo.lvh.me:3001/admin/courses/<slug>/settings).

Mutations call updateCourseFeatureFlagsAction, write admin.course.flags.update audit events, and revalidate course:<slug> cache tags.

Read path (learners)

The shared access seam resolveEntryAccess loads isSequentialGatingEnabled for the route slug, then passes the boolean into computeGateStates from gating-core.ts. The resulting gateStates are consumed by the entry sidebar, quiz/complete server actions, and the enrolled course-outline on /learn/course/\[slug\]. See entry-pagination.md.

Catalog listing (/learn), course landing visibility (/learn/course/\[slug\]), and enrollment (/learn/course/\[slug\]/enroll) all require catalog.listed=true for the current tenant unless the learner already has an active/completed enrollment. This keeps already-enrolled learners working while hiding unlisted courses from discovery and new enrollments.

The home page featured section resolves tenant-scoped flags from course_settings via src/lib/course/featured.ts and only shows courses where both catalog.featured=true and catalog.listed=true.

Because catalog.listed defaults to false, a tenant's catalog is empty until admins explicitly enable course visibility in /admin/courses/\[slug\]/settings.