Skip to content

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/tenantsConfigure/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:

  1. Exact match against tenants.custom_domain.
  2. Subdomain match against tenants.slug for <tenant>.<APP_BASE_DOMAIN>.
  3. Apex or www host resolves to the public marketing site with no tenant context.
  4. 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: 30
  • Cache-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.ts checks 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_version for every user in the tenant when status changes to suspended or archived.
  • 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 503 as 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.