ArchitectureControl plane (panel vs admin-web)

Control plane (panel vs admin-web)

One-line: the panel is the company’s control plane (operate all schools); admin-web is tenant-only (operate one school). Keeping them separate keeps operator power out of the tenant app.

Two web apps, two audiences

panel (super-admin-panel)admin-web (school-admin-panel)
Used byNexSchool operatorsA school’s own staff
Scopeall tenantsone tenant
API surface/api/platform/* (bypasses tenant resolution)/api/* (tenant-scoped)
Authplatform-admin JWT (@platform_admin_required)tenant user JWT + RBAC

The split is deliberate: operator-only capabilities (create tenants, assign plans, seed schools, impersonate) live only in the panel, so they can’t leak into the app a school admin uses.

What lives in the panel

  • Tenant lifecycle — create/suspend/soft-delete tenants, tenant search.
  • Plans — assign a plan and its features_json (which modules a tenant can use).
  • Onboarding / seeding — white-glove tenant setup; the structured school-setup flow (/api/school-setup/*) is super-admin-only (the tenant Admin role no longer carries school_setup.*).
  • Add-admin with one-time credential reveal — a temp password shown once, never audited.
  • Audit logs and platform settings.

A platform super-admin can enter any tenant’s admin-web with their own credentials:

  • God-login — on the tenant login path, if no tenant user matches, authenticate_platform_admin is tried (searches is_platform_admin across tenants with scoping cleared). On success the admin gets permissions = ["system.manage"], bypasses email_verified/setup gates, and the entered tenant is reported so subsequent requests scope to it. has_permission short-circuits True for platform admins.
  • One-click “Open admin-web” (panel → tenant) — no password re-entry. Backed by a Redis one-time code (modules/auth/handoff.py): single-use (atomic GET+DEL), ~90s TTL, sha256-hashed at rest, fail-closed (no Redis → no issue/redeem). POST /api/platform/tenants/<id>/login-link mints it; POST /api/auth/login-link/redeem consumes it via the shared _finalize_login path (identical response to password login). Possession of a valid code is the credential — only a platform admin can mint one.
⚠️

Both flows build links from ADMIN_WEB_BASE_URL. Set it in production, or login links point at localhost.

Why this matters

Before the split, onboarding and impersonation lived partly in the tenant app. Concentrating operator power in the panel — and making school-setup super-admin-only — means a compromised or curious school admin can’t reach cross-tenant or structural controls. It also gives operations a single, auditable place to run white-glove onboarding for the Gujarat pilot.