Tenant Configuration¶
Status: Design — authoritative for tenant feature flags, custom domains, and suspension behavior.
This document extends the multi-tenant model described in user-management.md and implementation.md. Tenant configuration is owned by the LMS database because it controls authentication, route access, and billing-adjacent behavior that cannot depend solely on the CMS site document.
Data model¶
The tenants table includes operational configuration alongside display identity:
tenants(
id bigserial primary key,
slug text not null unique,
name text not null,
status text not null check (status in ('active','suspended','archived')),
feature_flags_json jsonb not null default '{}',
custom_domain text null unique,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
suspended_at timestamptz null
)
slug remains the canonical tenant identifier. custom_domain is an optional alias that maps a fully qualified host such as training.acme.com to the same tenant row.
Feature flags¶
Tenant feature flags gate product capabilities, not security checks. Security must still be enforced by role, tenant scope, enrollment state, and published status.
Initial flags:
| Flag | Default | Purpose |
|---|---|---|
paidCoursesEnabled |
false |
Enables paid checkout surfaces and payment webhooks. |
certificatesEnabled |
false |
Enables certificate issuance after completion. |
reviewsEnabled |
false |
Enables learner reviews and public rating display. |
qnaEnabled |
false |
Enables entry-level Q&A surfaces. |
analyticsEnabled |
true |
Enables admin dashboard analytics. |
customDomainsEnabled |
false |
Allows custom-domain assignment for the tenant. |
magicLink |
true |
Learner magic-link sign-in CTA and /learn/magic routes. |
Wired flags (in src/lib/tenant/feature-flag-registry.ts) can be changed in the admin UI. Per-course behaviour such as sequential gating and home-page featuring is configured in course-configuration.md under tenant-scoped course_settings. Documented platform flags such as paidCoursesEnabled remain schema-only until the corresponding product code reads them.
Feature flags are read server-side via getTenantFeatures(tenantId) in features.ts. Client components receive resolved booleans as props; they never see raw JSON.
Admin management¶
| Role | Surface | Capability |
|---|---|---|
super_admin |
/admin/tenants → Configure → /admin/tenants/[tenantId] |
All catalog flags + suspend/unsuspend |
tenant_admin |
/admin/settings |
Tenant-tier flags only (magicLink) |
Mutations call updateTenantFeatureFlagsAction, write admin.tenant.flags.update audit events, and revalidate tenant-host:<slug> / tenant:<id> cache tags.
Host resolution¶
proxy.ts resolves tenants in this order:
- Exact match against
tenants.custom_domain. - Subdomain match against
tenants.slugfor<tenant>.<APP_BASE_DOMAIN>. - Apex or
wwwhost resolves to the public marketing site with no tenant context. - Unknown host returns the public 404 page.
Host resolution is cached for 60 seconds and tagged by tenant:<id> or tenant-host:<host> so activation, suspension, or custom-domain changes can invalidate stale host mappings.
Transient database failures¶
When tenant resolution or protected-route session-state checks fail due to transient database connectivity (for example Neon cold-start or pool pressure), proxy.ts returns a retryable 503 Service Unavailable response with:
Retry-After: 30Cache-Control: no-store
This separates "unknown host" (404) from temporary infrastructure unavailability (503) and prevents CDN/browser caching of failure responses.
Custom domains¶
Only super_admin users can create or remove custom-domain assignments. A tenant admin may request a custom domain, but the assignment is not active until the platform verifies DNS ownership.
Minimum lifecycle:
| Status | Meaning |
|---|---|
requested |
Tenant admin requested a hostname. |
verifying |
Platform is waiting for DNS proof. |
active |
Host maps to the tenant in proxy.ts. |
failed |
Verification failed or timed out. |
removed |
Host is no longer accepted for the tenant. |
If this lifecycle outgrows the single tenants.custom_domain column, introduce a tenant_domains table with (tenant_id, hostname, status, verified_at, created_at) and keep tenants.custom_domain as a cached primary domain only if it reduces query cost.
Suspension and archival¶
Tenant status controls route access:
| Status | Runtime behavior |
|---|---|
active |
Normal routing and session behavior. |
suspended |
proxy.ts returns a branded 503 for tenant routes and blocks sign-in. |
archived |
Tenant is hidden from normal routing; only super_admin audit views may access it. |
Suspension rules:
proxy.tschecks tenant status before auth routing. Suspended tenants do not render LMS, admin, public course, or CMS-backed tenant pages.- Active sessions are invalidated by bumping
users.token_versionfor every user in the tenant when status changes tosuspendedorarchived. - Background jobs and webhooks must no-op for suspended tenants except audit logging and payment-provider cleanup.
- Public well-known files for suspended tenants return the same
503as tenant pages unless a specific legal/security route is required by policy.
Super-admin flows¶
super_admin users can:
- Create tenants with default feature flags.
- Activate or suspend tenants.
- Assign, verify, and remove custom domains.
- Toggle tenant feature flags.
- View archived tenant audit logs through
withTenantOverride().
Every mutation writes audit_log with target_kind='tenant', target_id=tenants.id, and metadata that includes changed fields but excludes secrets.
Testing¶
- Unit-test host resolution for subdomain, custom domain, apex, unknown host, and suspended tenant.
- E2E-test suspended tenant routing: public page, sign-in, dashboard, admin, and course routes return
503. - Verify token invalidation after suspension by signing in, suspending the tenant, and confirming the next authenticated request is rejected.
- Test feature flags server-side and in UI props; client bundles must not include raw
feature_flags_json.