Skip to content

Multi-Tenant Administration

Audience: super_admin operators provisioning, suspending, and managing tenants. Tenant admins managing their own tenant.

Scope: How the LMS resolves tenants, how super_admin operates across tenants, and the lifecycle states. Authoritative behaviour lives in reference/tenant-configuration.md.

The LMS is multi-tenant by host. One database, many tenants, isolated by tenant_id in every query.

Host Resolution

src/proxy.ts resolves tenants in this order:

  1. Exact custom-domain match in tenants.custom_domain.
  2. Subdomain match of <slug>.<APP_BASE_DOMAIN> (e.g. acme.lvh.me:3001tenants.slug = 'acme').
  3. Apex / www — public marketing site (no tenant context).
  4. Unknown host — 404.

Suspended tenants return a branded 503 before auth routing runs. Archived tenants are read-only — sign-in fails and learner routes return 410.

Local development uses lvh.me (or any *.localhost you prefer) so subdomain routing works without /etc/hosts edits.

Tenant Lifecycle

tenants.status ∈ { 'active', 'suspended', 'archived' }:

Status Behaviour
active Normal operation.
suspended All routes return 503. super_admin can re-activate via /admin/tenants.
archived Sign-in disabled, learner routes 410. Data retained per legal/retention policy.

Every tenant transition writes an audit_log row. Suspending a tenant bumps every users.token_version for that tenant so existing sessions are invalidated.

Feature Flags

tenants.feature_flags_json stores per-tenant booleans. Canonical keys and defaults are listed in reference/tenant-configuration.md. Per-course flags (e.g. sequential gating) live in course_settings — see reference/course-configuration.md. Read server-side helpers such as isMagicLinkEnabled and isSequentialGatingEnabled; client components receive resolved booleans as props — they never see the raw JSON.

magicLink: when false, hides the sign-in magic-link CTA, redirects /learn/magic to sign-in, and blocks requestMagicLinkAction. Admin invitation emails to /invite/\[token\] are not affected (separate admin flow).

Custom Domains

tenants.custom_domain (nullable) holds the apex/host alias served alongside the tenant subdomain. The proxy treats it as an exact match; learners on the custom domain see the same content as the subdomain.

Super-Admin Operations

super_admin has tenant_id = null and uses withTenantOverride(tenantId, () => ...) to scope cross-tenant queries explicitly. Without an override the helpers refuse — there is no implicit "all tenants" mode.

Surface Action
/admin/tenants Create tenants, search, suspend/restore. Open Configure for per-tenant feature flags.
/admin/tenants/[tenantId] Tenant detail: status toggle and all registered feature flags (super_admin).
/admin/users with tenant filter Cross-tenant user search.
Impersonation Switch into a target user's session for audited support.

Impersonation requires browsing the target tenant's host before starting. See Impersonation for the operator workflow and development/auth.md for the technical contract.

Tenant Admin Operations

Tenant admins manage everything inside their own tenant — never across tenants.

Surface Action
/admin/users Promote within { student, course_admin, tenant_admin }; deactivate; force sign-out everywhere.
/admin/users/invite Invite a student, course admin, or tenant admin to the current tenant.
/admin/users/invitations Search invitations and resend or revoke invitations that have not been accepted.
/admin/courses/\[slug\]/admins Assign / revoke course_admin_assignments.
/admin/settings Edit tenant-tier feature flags (magicLink) for the signed-in tenant.
/admin/courses/\[slug\]/settings Edit course-tier feature flags on a tenant subdomain (not apex lvh.me).

Tenant admins cannot create or suspend tenants. Promotion of a tenant_admin to super_admin is intentionally not possible from the UI — that role is set by an out-of-band admin process.

For role selection, course behavior, recipient acceptance, invitation statuses, resend/revoke actions, and troubleshooting, see Invite and manage users.