panel (dev)
The super-admin control-plane frontend: a separate Next.js app for NexSchool platform operators to manage tenants, plans, and audit logs against /api/platform/*.
App structure
The panel (panel/) is a standalone Next.js 16 App Router app — same stack as admin-web (React 19, TypeScript 5, Tailwind 4, TanStack Query 5, React Hook Form + Zod, Radix UI, Sonner) but with no Firebase (platform admins get no push). It runs on port 3001 locally; production is panel.nexschool.com.
Routes live under panel/src/app/, split into two route groups:
panel/src/app/
├── layout.tsx
├── globals.css
├── (auth)/
│ └── login/page.tsx
└── (dashboard)/
├── layout.tsx
└── dashboard/
├── page.tsx Platform overview
├── settings/page.tsx Global platform settings
├── tenants/
│ ├── page.tsx Tenant list
│ └── [id]/
│ ├── page.tsx Tenant detail
│ └── tenant-detail-view.tsx
├── audit/page.tsx Audit log viewer
└── notification-templates/
└── page.tsx Platform notification templatesSupporting folders: components/ (forms/, layout/, providers/, tables/, ui/), services/ (API clients for /api/platform/*), and types/.
The dashboard landing page (/dashboard) surfaces total tenants, active vs suspended counts, new signups this month, and recent audit entries.
Platform API & auth
The panel calls the same Flask API as admin-web but only hits /api/platform/* and /api/auth/*. There is no tenant-scoped data here, so — unlike admin-web — panel requests do not send X-Tenant-ID, and the TanStack Query tenant-scoping rules do not apply.
Login uses the shared /api/auth/login endpoint with a platform flag:
// Panel uses a separate auth store. No X-Tenant-ID header for platform routes.
const response = await api.post('/api/auth/login', {
email,
password,
is_platform: true, // signals platform admin login
});
// JWT payload: { user_id, is_platform_admin: true }The backend authorizes /api/platform/* routes by checking the is_platform_admin flag in the JWT — not by tenant membership.
Endpoints the panel uses:
Authentication:
POST /api/auth/login (platform admin credentials)
POST /api/auth/logout
Platform management:
GET /api/platform/tenants
POST /api/platform/tenants
GET /api/platform/tenants/<id>
PUT /api/platform/tenants/<id>
POST /api/platform/tenants/<id>/suspend
POST /api/platform/tenants/<id>/activate
GET /api/platform/plans
POST /api/platform/plans
PUT /api/platform/plans/<id>
GET /api/platform/audit-logs
GET /api/platform/notification-settings
PUT /api/platform/notification-settings
GET /api/platform/notification-templates
POST /api/platform/notification-templates
PUT /api/platform/notification-templates/<id>Key env vars:
NEXT_PUBLIC_API_URL=http://localhost/api
NEXT_PUBLIC_GATEWAY_ORIGIN=http://panel.localhostTenant management
/dashboard/tenants lists every tenant (school) with:
- Search by name, subdomain, or contact email — server-side via
?search=onGET /api/platform/tenants(case-insensitive contains). The input is debounced and resets to page 1 on change. - A Create new tenant action.
- A status filter exists on the API (
?status=) but is not yet exposed in the UI.
/dashboard/tenants/[id] (rendered through tenant-detail-view.tsx) shows school information, current plan assignment, feature-flag overrides, subscription status, per-tenant student/teacher/class counts, recent audit logs for that tenant, and Suspend / Activate actions (backed by POST .../suspend and POST .../activate).
Tenant shape (panel view):
{
"id": 5,
"name": "Greenfield Academy",
"subdomain": "greenfield",
"status": "active",
"plan": { "id": 2, "name": "growth" },
"is_setup_complete": true,
"trial_ends_at": null,
"contact_email": "admin@greenfield.edu",
"feature_flags": {},
"stats": {
"total_students": 450,
"total_teachers": 32,
"total_classes": 18
},
"created_at": "2024-01-15T09:00:00Z"
}Plans
Subscription plans are managed platform-wide via GET/POST /api/platform/plans and PUT /api/platform/plans/<id>, and assigned to tenants from the tenant detail view. A plan defines pricing and capacity limits plus a feature set:
{
"id": 2,
"name": "growth",
"price_monthly": 2999.00,
"max_students": 1000,
"max_teachers": 100,
"features_json": {
"features": ["attendance", "fees_management", "timetable", "transport", "notifications"]
}
}The features list drives the tenant’s feature flags; see /product/subscriptions for plan/tier semantics.
Audit
/dashboard/audit is a read-only platform audit trail — who did what to which tenant — filterable by tenant, action type, and date range, sourced from GET /api/platform/audit-logs. Entries are non-editable.
Impersonation (login link)
From the tenant detail view, an operator can open the tenant’s admin-web in one click (“open admin-web” login link), so support and setup work can be done as that tenant without a manual re-login. This crosses the control-plane boundary from panel into the tenant app; the flow and its guardrails are described in /architecture/control-plane.