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
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. -
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/marketingscreen renders the banner list on the default host; the Banner menu entry also appears for HistoryClass tenant admins. -
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. 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. -
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_strategyper tenant, and HistoryClass renders the shared template with its own hero, featured courses and branding on desktop and mobile. Open: beta has nolanding.strategyconfig 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. -
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.
Tenant-grade product surfaces
-
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, plustenant_idon 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.
Verified custom domains
-
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.
Hardening and debt cleanup
-
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_adminboolean 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.
- 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-23 — Phase 2 beta-verified: all seven tickets verified on beta against merge commit 83eecba1 — 24 automated checks passed (API, database, logs and browser evidence); full detail in the beta verification report. Highlights: activation gate returns 409 with every missing prerequisite; staff invitations work end to end including revoked/duplicate/wrong-account rejection paths; HistoryClass serves the de-branded shared template over HTTPS. Jira SP-609/557/610/611/476/612/613 moved to Unrelease. Findings filed: SP-623 (footer year hardcoded to 2025, minor) and SP-624 (DO CDN can serve a stale
index.htmlfor up to 24 h after a deploy). SP-477 (Phase 4) is now unblocked. - 2026-08-23 — Phase 2 merged: SP-609, SP-557, SP-610, SP-611, SP-476, SP-612 and SP-613 merged to main as stacked PRs #288–#294 (de-branded frontend shell, default banner scoping, config-driven landing strategy, launch-quality tenant landing template, managed-subdomain activation gate plus the tenant-activation rehearsal runbook, staff invitations backend and UI). Auto-deployed to beta; not yet beta-verified and not yet in a production release. HistoryClass's
landing.strategyconfig decision is still open. Next step is the HistoryClass beta rehearsal per the new runbook. - 2026-08-21 — Phases 3–5 ticketed: SP-614–SP-616 (email, legal, payments), SP-617–SP-619 feeding SP-477 (custom domains), SP-620–SP-621 feeding SP-478 (hardening). Every phase now has a stacked-PR chain.
- 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.