SkillPixel LMS — Multi-Tenant Roadmap

From single-tenant SkillPixel to safely serving multiple tenants — audit date 2026-08-17, epic SP-371

Last updated 2026-08-21 · v0.0.37 in production · Phase 1 merged to main, beta-verified 2026-08-20

Core multi-tenant plumbing is done. What is left is ordered into six phases.

The tenant model, host routing, query scoping, bootstrap API, platform-admin CRUD, feature gating, theming and storage namespacing are all in place.

The remaining work: release what is merged, close activation blockers, activate managed subdomains, make product surfaces tenant-grade, add custom domains, then harden and clear debt.

Ordering principle: security and correctness first, then first-tenant launch, then polish and debt.
Legend SP-xxxexisting Jira ticket NEW TICKET DECISION NEEDED SECURITY BETA-VERIFY SHIPPED v0.0.37 MERGED · UNRELEASED MERGED · BETA-VERIFIED
Phase 0 — now

Beta-verify what is already merged

GoalBeta-verify the merged code — much of it is already live after v0.0.37 — and clear the Testing column; everything later builds on it.
Why firstVerifying now avoids stacking new work on unverified code.
1 of 4 items shipped
  • Beta-verify tenantization phases 05–12

    Storage and media, enrollments and certificates, submissions and grading, commerce, platform-admin APIs and UI, ops surfaces, frontend bootstrap.

  • Beta-verify routing, authz and branding hardening

    Domain-aware URLs, tenant-scoped roles, branding and SEO in bootstrap, slug at provisioning, self-serve settings.

  • Beta-verify feature gating

    Config-backed entitlements plus the feature matrix UI. The backend and frontend gating code (SP-586, SP-587) is live in production as of v0.0.37 — what remains is beta verification of the gating behaviour itself, plus SP-585's rollout decisions. PR #276 (2026-08-19) centralized frontend gating into a TenantFeature enum with enabled_features sanitized at a single choke point — including the sessionStorage cache — hardening the surface before beta verification. MERGED · UNRELEASED

    SP-585 SP-586SHIPPED v0.0.37 SP-587SHIPPED v0.0.37 BETA-VERIFY
  • Platform-admin overhaul released

    Shipped to production in v0.0.37 on 2026-08-18, together with SP-595 and SP-596 moving platform-admin fully onto the /platform API behind a centralized guard, off direct DB access. Jira status still needs moving.

    SP-590SP-595SP-596 SHIPPED v0.0.37
Phase 1 — security + correctness

Close activation blockers

GoalMake it safe to turn on a second tenant at all. The six tickets are merged and beta-verified (2026-08-20); some items fold into SP-476 scope.
StackThe six tickets were merged to main as a stacked-PR chain (SP-602 → SP-607) and beta-verified 2026-08-20; SP-476 is now unblocked; SP-478 Gate C's 7-day zero-fallback-write window started 2026-08-20.
Why nowThese are silent failure modes that get worse the moment real tenant traffic exists.
StatusMerged to main 2026-08-20 as PRs #277–#282; live on beta (revision 00247) and verified the same day. SP-476 is now unblocked; SP-478 Gate C's 7-day zero-fallback-write window started 2026-08-20.
6 of 6 merged · beta-verified 2026-08-20 · awaiting production release
  • Stop signup trusting X-Tenant-Host (stack 1/6)

    Profile sync auto-creates a STUDENT membership in whatever tenant the client-sent header names (apps/backend/app/services/user.py), so any logged-in user can self-join any active tenant. The single biggest blocker. Beta-verified 2026-08-20: a closed tenant no longer gains a membership from the header; an open-enrollment tenant still does.

    SP-602SECURITYMERGED · BETA-VERIFIED
  • Fallback-write metric for hot-table defaults (stack 2/6)

    Courses, chapters, lessons and questions still carry a default-tenant server default. The metric that gates its later removal — zero fallback writes for 7 days — is merged and beta-verified (2026-08-20) and feeds the SP-478 Gate C criterion, whose window is now running. Beta-verified 2026-08-20: tenant_default_fallback_write is wired and has logged zero events since deploy.

    SP-603MERGED · BETA-VERIFIED
  • Default entitlements at tenant creation (stack 3/6)

    create_tenant writes only the tenant row, so all 10 gated features 404 until an operator flips them by hand — this already bit HistoryClass. Decided default (2026-08-20): new tenants start with certificates and DOCX import enabled, all other gated features disabled. Beta-verified 2026-08-20: a new tenant is seeded with all 10 feature rows (certificates and DOCX import on, the other eight off); deleting a tenant also removes its config rows. Tenants created via the platform API start PROVISIONED, so their domains only route once the tenant is set ACTIVE.

    SP-604MERGED · BETA-VERIFIED
  • Reminder crons fan out over all active tenants (stack 4/6)

    Notification crons bind only the default tenant, so other tenants get no deadline or live-class reminders (apps/backend/app/routers/v1/notification.py). Beta-verified 2026-08-20: a manual cron run completed with cron_fanout_completed tenant_count=3 and failed_count=0.

    SP-605MERGED · BETA-VERIFIED
  • Harden the domain write path (stack 5/6)

    Create and update only lowercase the value. Add IDNA/punycode canonicalization to match the read path, plus a reserved and platform-label blocklist (apps/backend/app/schemas/platform.py). Beta-verified 2026-08-20: uppercase and whitespace are canonicalized, Unicode is stored as punycode, reserved labels (www/api/beta/apex/localhost) return 422, PLATFORM type returns 422, and duplicates return 409.

    SP-606MERGED · BETA-VERIFIED
  • Cross-instance domain cache invalidation (stack 6/6)

    Domain activate and deactivate clear only the local process cache; other instances serve stale routing for up to 5 minutes. Beta-verified 2026-08-20: domain activate and disable flip routing instantly, and the Redis generation key bumps on every domain write.

    SP-607MERGED · BETA-VERIFIED
  • Decide and fix the RSA environment

    Dropped 2026-08-20 — RSA is a separate deployment with a long-diverged codebase; not part of this rollout.

    DROPPED
Phase 2 — first tenant live

Activate managed subdomains and minimum launch polish

GoalFirst non-default tenant (HistoryClass, SP-542) live on a managed subdomain.
StackTicketed 2026-08-21 as a seven-item stacked-PR chain — SP-609 → SP-557 → SP-610 → SP-611 → SP-476 → SP-612 → SP-613 — with the pre-existing SP-557 and SP-476 folded in; SP-476 blocks SP-477 (Phase 4).
Why hereActivation is only safe after Phase 1; the polish items are what its users see on day one.
NoteHistoryClass content work (SP-549) already shipped in v0.0.37 — this phase is what stands between it and a live tenant site.
  • Activate managed subdomain routing (stack 5/7)

    Browser hostname pass-through, tenant activation gates, audit queries, monitoring. Default-tenant traffic must stay stable. SP-607 is merged and beta-verified (2026-08-20), so this work is unblocked.

  • Fix default banner branding (stack 2/7)

    Default banner components hardcode SkillPixel imagery and copy on shared surfaces. Also add a uniqueness guard against duplicate seeded default banners.

  • De-brand the frontend shell (stack 1/7)

    A static index.html title and meta is served to every tenant; the hardcoded default-tenant snapshot and fallback hosts in resolution.ts must derive from config.

  • Tenant landing pages to launch quality (stack 3/7 + 4/7)

    Split in two. SP-610 (stack 3/7) — drive the landing strategy from tenant config instead of the hardcoded build-time UUID map, which holds only the local seed UUID. SP-611 (stack 4/7) — bring the shared template to launch quality: hero copy, announcement and featured courses (frontend phase-3 WP3–WP6).

  • Staff invitation flow (stack 6/7 + 7/7)

    Memberships 404 unless the user already self-registered. SP-612 (stack 6/7) — backend: invite tokens, a pending membership state, an accept endpoint and the invite email. SP-613 (stack 7/7) — UI: platform-admin invite screen, the tenant-admin menu entry and the accept page. If launch comes early, these two can slip after activation and land early in Phase 3, with the manual workaround accepted for launch.

Phase 3 — product surfaces

Tenant-grade product surfaces

GoalA tenant's users never see SkillPixel branding, legal identity, or SkillPixel-only behavior.
Why hereNot needed to flip the switch, but needed before a tenant runs a real cohort.
  • Per-tenant email

    One global SMTP identity (info@skillpixel.vn) and SkillPixel-branded templates and subjects everywhere. Add a per-tenant from-identity and branding, plus tenant_id on email templates.

    NEW TICKET
  • Per-tenant legal pages

    Privacy, terms, refund and company address are shared i18n copy naming SkillPixel's legal entity. Decide: tenant data, per-tenant i18n, or external links.

    NEW TICKETDECISION NEEDED
  • Tenant-aware payments

    Payment and cart routes cannot be feature-gated naively — webhooks resolve to the default tenant after the gateway already charged — and provider config is a hardcoded TODO. Decide: provider list entitlement, or per-tenant merchant credentials.

    NEW TICKETDECISION NEEDED
  • Backend-backed localisations

    In progress. Per-tenant copy overrides fit naturally alongside the email and legal work.

Phase 4 — custom domains

Verified custom domains

GoalCustomer-owned domains served safely (SP-477).
Why hereThe spec sequences this strictly after managed subdomains are stable.
  • Domain verification flow

    TXT/CNAME challenge, with activation requiring a verified domain. Today ACTIVE can be set with no verification and no external side effects.

  • Firebase authorized-domain automation and guardrails

    The spec assumes under 50 active custom domains, warns at 75 and blocks at 90. No counter or guardrail exists in code.

  • Cloudflare custom hostnames and canonical-host rules

    Registration on activate and deactivate, predictable canonical redirects, managed subdomain as the fallback.

Phase 5 — hardening

Hardening and debt cleanup

GoalRemove compatibility layers and add defense in depth (SP-478, SP-573 and small items).
Why lastNeeds runtime evidence from real multi-tenant traffic to do safely.
  • Finish tenant-scoped authorization

    The contest service and the admin chat WebSocket still authorize on global Firebase claims. Add wrong-tenant 403 tests per router family.

  • Audit trail and scoped platform-admin

    One is_platform_admin boolean controls every tenant's entitlements, with no append-only log of config writes.

    NEW TICKET
  • Remove hot-table defaults and phased RLS

    Drop default-tenant stamping once the Phase-1 metric shows 7 clean days (SP-603 is merged and beta-verified 2026-08-20; Gate C's 7-day window is now running), add PostgreSQL RLS for prioritized tenant tables, and reduce global roles to platform-wide semantics.

  • Small items sweep

    Banner uploads missing a tenant prefix, the last direct-Postgres read in the platform-admin activity route, a CORS wildcard revisit, and cron tenant-binding as a dependency instead of a convention.

    NEW TICKET

Open decisions

Each one blocks or shapes a phase below.

  1. Default entitlement set for a new tenant. Resolved 2026-08-20 — new tenants start with certificates and DOCX import enabled, all other gated features disabled. Implemented by SP-604. No longer blocks Phase 1
  2. RSA tenancy model — tenant, separate deployment, or new environment. Resolved 2026-08-20 — RSA stays a separate deployment with a long-diverged codebase; dropped from this rollout. No longer blocks Phase 1
  3. Tenant legal identity ownership. Blocks Phase 3
  4. Payment provider model per tenant. Blocks Phase 3
  5. Cross-tenant identity policy — users are global, and membership-add errors leak email existence. Informs the invitation flow

Changelog