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.
Beta-verify what is already merged
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
TenantFeatureenum withenabled_featuressanitized at a single choke point — including the sessionStorage cache — hardening the surface before beta verification. MERGED · UNRELEASED -
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
/platformAPI behind a centralized guard, off direct DB access. Jira status still needs moving.
Close activation blockers
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. -
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_tenantwrites 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
Activate managed subdomains and minimum launch polish
-
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.htmltitle and meta is served to every tenant; the hardcoded default-tenant snapshot and fallback hosts inresolution.tsmust 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.
Tenant-grade product surfaces
-
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, plustenant_idon 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.
Verified custom domains
-
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.
Hardening and debt cleanup
-
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_adminboolean 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.
- 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
- 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
- Tenant legal identity ownership. Blocks Phase 3
- Payment provider model per tenant. Blocks Phase 3
- Cross-tenant identity policy — users are global, and membership-add errors leak email existence. Informs the invitation flow
Changelog
- 2026-08-21 — Phase 2 ticketed: SP-609–SP-613 created; stacked chain SP-609 → SP-557 → SP-610 → SP-611 → SP-476 → SP-612 → SP-613; SP-476 now blocks SP-477 (custom domains).
- 2026-08-20 — Phase 1 merged + beta-verified: SP-602–SP-607 merged to main as stacked PRs #277–#282, auto-deployed to beta and verified live the same day (signup guard, fallback metric, default entitlements, cron fan-out, domain write hardening, cross-instance cache invalidation). Not yet in a production release. SP-476 unblocked; SP-478 Gate C 7-day window started.
- 2026-08-20 — Phase 1 ticketed: SP-602–SP-607 created as a stacked-PR chain gating SP-476 (SP-603 also gates SP-478). Decided: new tenants start with certificates + DOCX import; RSA dropped (separate deployment).
- 2026-08-19 — Merged to main (unreleased): centralized frontend tenant feature registration —
TenantFeatureenum, sanitizedenabled_featureschoke point, unifiedFeatureRoute(PR #276). - 2026-08-18 — v0.0.37 to production: platform-admin overhaul (SP-590), per-tenant feature gating live (SP-586, SP-587), platform-admin fully on
/platformAPI (SP-595, SP-596), History class content (SP-549). - 2026-08-17 — Initial roadmap from code audit + Jira epic SP-371.