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-23 · v0.0.37 in production · Phase 1 merged and beta-verified 2026-08-20 · Phase 2 merged 2026-08-23 and beta-verified the same day (report)

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 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.
StackThe seven tickets were merged to main as a stacked-PR chain (SP-609 → SP-557 → SP-610 → SP-611 → SP-476 → SP-612 → SP-613) on 2026-08-23, with the pre-existing SP-557 and SP-476 folded in; SP-476 unblocks SP-477 (Phase 4) once this phase is beta-verified.
Why hereActivation is only safe after Phase 1; the polish items are what its users see on day one.
StatusMerged to main 2026-08-23 as PRs #288–#294 and beta-verified the same day — 24 automated checks passed across all seven tickets; full evidence with screenshots in the beta verification report. Jira moved to Unrelease. Findings: SP-623 (hardcoded footer year, minor) and SP-624 (CDN can serve a stale index.html for up to 24 h after a deploy). Not yet in a production release.
NoteHistoryClass content work (SP-549) already shipped in v0.0.37 — this phase is what stands between it and a live tenant site.
7 of 7 merged · beta-verified 2026-08-23 · awaiting production release
  • 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. Merged 2026-08-23 together with the activation runbook (docs/tenant-activation-runbook.md). Beta-verified 2026-08-23: activating a tenant with no domain and no tenant admin returns 409 listing both missing prerequisites; HistoryClass stays healthy (tenant and primary managed subdomain ACTIVE and verified, served over HTTPS); zero default-tenant fallback writes and zero backend errors since the deploy. SP-477 (Phase 4) is now unblocked.

    SP-476PR #292MERGED · BETA-VERIFIED
  • 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. Beta-verified 2026-08-23: public and admin banner APIs return 200 and the /admin/marketing screen renders the banner list on the default host; the Banner menu entry also appears for HistoryClass tenant admins.

    SP-557PR #289MERGED · BETA-VERIFIED
  • 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. Beta-verified 2026-08-23: the served shell contains zero SkillPixel references; after bootstrap the HistoryClass page reports its own title, description and og:site_name with no SkillPixel body text.

    SP-609PR #288MERGED · BETA-VERIFIED
  • 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). Beta-verified 2026-08-23: bootstrap exposes landing_strategy per tenant, and HistoryClass renders the shared template with its own hero, featured courses and branding on desktop and mobile. Open: beta has no landing.strategy config row for HistoryClass, so it renders the shared template via the deterministic non-default fallback — the bespoke-page-versus-template decision still needs an explicit config row either way.

    SP-610PR #290SP-611PR #291MERGED · BETA-VERIFIEDDECISION NEEDED
  • 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. Both merged 2026-08-23, so no manual membership workaround is needed at launch. Beta-verified 2026-08-23 end to end on HistoryClass: invite → pending → accept as the invitee → membership with the invited role; duplicate, revoked and bogus tokens are rejected with the right statuses; the tenant-admin screen, platform-admin tab and both accept-page states render correctly. Real email delivery stays blocked on SP-614.

    SP-612PR #293SP-613PR #294MERGED · BETA-VERIFIED
Phase 3 — product surfaces

Tenant-grade product surfaces

GoalA tenant's users never see SkillPixel branding, legal identity, or SkillPixel-only behavior.
StackTicketed 2026-08-21 as stacked PRs SP-614 → SP-615 → SP-616; SP-615 and SP-616 wait for their decisions before implementation.
Why hereNot needed to flip the switch, but needed before a tenant runs a real cohort.
  • Per-tenant email (stack 1/3)

    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.

  • Per-tenant legal pages (stack 2/3)

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

    SP-615DECISION NEEDED
  • Tenant-aware payments (stack 3/3)

    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.

    SP-616DECISION 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).
StackTicketed 2026-08-21 as stacked PRs SP-476 → SP-617 → SP-618 → SP-619 → SP-477, with the Phase-2 SP-476 at the head and SP-477 as the final activation step. SP-476 merged 2026-08-23 (PR #292), so the chain starts at SP-617 once Phase 2 is beta-verified.
Why hereThe spec sequences this strictly after managed subdomains are stable.
  • Domain verification flow (stack 1/4)

    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 (stack 2/4)

    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 (stack 3/4)

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

  • Custom-domain activation (stack 4/4)

    SP-477 is the final activation step, landing once SP-617, SP-618 and SP-619 are merged.

Phase 5 — hardening

Hardening and debt cleanup

GoalRemove compatibility layers and add defense in depth (SP-478, SP-573 and small items).
StackTicketed 2026-08-21 as stacked PRs SP-573 → SP-620 → SP-621 → SP-478; SP-478 closes the phase and stays gated on the SP-603 fallback-write metric reading zero for 7 days.
Why lastNeeds runtime evidence from real multi-tenant traffic to do safely.
  • Finish tenant-scoped authorization (stack 1/4)

    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 (stack 2/4)

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

  • Small items sweep (stack 3/4)

    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.

  • Remove hot-table defaults and phased RLS (stack 4/4)

    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.

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