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.
Related¶
tenant-configuration.md— tenant-tier flags (magicLinkonly in the admin catalog).entry-pagination.md— gating UX and completion semantics.