23 KiB
Nextgen LMS — National Scale Multi-Tenant & SuperAdmin Licensing Platform Documentation
Executive Summary
This document provides full technical, architectural, operational, and development-testing documentation for the Nextgen LMS Multi-Tenant Ecosystem, the Standalone SuperAdmin Governance Portal, the Cryptographic Termly Licensing Engine, the Multi-School Instance Spawner, and the Event-Sourced Sync Engine.
[!IMPORTANT] Application Identity: This SuperAdmin portal is part of the NextGen LMS platform (formerly referenced as Africa Alert). Within
superadmin/the rename is complete: the browser title, sidebar header,/api/healthservice name, license token issuer, andpackage.jsondescriptions now all read NextGen LMS. Broaderclient/andserver/surfaces still carry the Africa Alert string pending a coordinated cutover; do not consider those authoritative.
1. System Architecture Overview
graph TD
subgraph SuperAdmin Governance Layer (e:\nextgen\next-gen\superadmin)
SA_Client["Standalone SuperAdmin Client (Port 3003)"] --> SA_Server["Standalone SuperAdmin Server (Port 3002)"]
SA_Server -->|Spawns & Provisions| Spawner["InstanceSpawner Orchestrator"]
SA_Server -->|Issues Signed HS256 JWT Tokens| LicGen["LicenseTokenGenerator"]
SA_Server -->|Direct Cloud Sync & Bucket Provisioning| SupabaseCloud["Supabase Cloud (@supabase/supabase-js)"]
end
subgraph Spawned Subscribing School Instances (Dev / Production)
Spawner -->|Spawned Server #1| SchoolServer1["School Server #1 (Port 3010 / DB #1)"]
Spawner -->|Spawned Server #2| SchoolServer2["School Server #2 (Port 3011 / DB #2)"]
Spawner -->|Spawned Server #N| SchoolServerN["School Server #N (Port 301X / DB #N)"]
end
subgraph School Instance Architecture (Template: server/ & client/)
SchoolServer1 --> AccessMW["Licensing & Access Middleware"]
AccessMW -->|Valid License / Grace Period| MainAPI["School Express API"]
AccessMW -->|Expired License| 402Block["402 Payment Required / Billing Prompt"]
MainAPI --> PostgresRLS["PostgreSQL DB with Row Level Security (RLS)"]
end
2. SuperAdmin Direct Integration with Supabase Cloud
- Direct Supabase Link (
SupabaseSuperAdminService.js):- SuperAdmin Server integrates with Supabase Cloud via
@supabase/supabase-jsusingSUPABASE_URLandSUPABASE_KEY(the anon / publishable JWT). - No service-role key is held by any process — neither the SuperAdmin nor any tenant server reads
SUPABASE_SERVICE_ROLE_KEY. The trust boundary is the Postgres function layer, not the JWT. - Cloud Multi-Tenant Sync: Tenant onboarding and license issuance call SECURITY DEFINER RPC functions (
upsert_tenant_cloud,upsert_license_cloud) installed by migration2026072900000100_superadmin_rpc_functions.js(raw SQL insql/20260729_superadmin_rpc_functions.sql). These functions validate the payload and perform the writes; they are GRANTed toanonandauthenticated. - Cloud RLS & Governance: Reads (cloud tenant list, per-tenant cloud state, cloud license list) go through the anon role under the RLS read policy from migration
2026072900000000_tenant_licenses_rls_read_policy. The application layer scopes bytenant_idas defence-in-depth.
- SuperAdmin Server integrates with Supabase Cloud via
2.1 Tenant License Distribution (Supabase-first)
Updated in
feature/supabase-license-fetch— licenses are now fetched directly from Supabase Cloud by the tenant server, then verified offline with the Ed25519 public key.
┌──────────────────┐ signed JWT ┌──────────────────┐ anon + RPC ┌──────────────────┐
│ SuperAdmin │ ─────────▶ │ Supabase Cloud │ ───────────▶ │ tenant_licenses │
│ (port 3002) │ │ (anon-readable) │ upsert_* │ table │
└──────────────────┘ └──────────────────┘ └──────────────────┘
▲
│ anon key + ?tenant_id=eq.<uuid>
│ 5s timeout, AbortSignal
▼
┌──────────────────┐
│ Tenant server │
│ licensingMW │ ──▶ Verify EdDSA against
│ (port 3001) │ SUPERADMIN_PUBLIC_KEY
└──────────────────┘
│
▼ on success
┌──────────────────┐
│ Local SQLite │ ← write-back cache
│ tenant_licenses │ (offline resilience)
└──────────────────┘
- Read path on the tenant server:
server/src/services/SupabaseLicenseService.jsconstructs a Supabase client with the anon key (SUPABASE_KEY/VITE_SUPABASE_ANON_KEY) and queriestenant_licensesfiltered by the request'stenant_id. 5s timeout viaAbortSignal.timeout. - Middleware ordering (
server/src/middleware/licensingMiddleware.js):- Cache (
LicenseCacheServiceLRU + optional Redis) — TTL clamped toexp - now. - Supabase fetch —
SupabaseLicenseService.fetchActiveLicense(tenantId). On success: verify EdDSA againstSUPERADMIN_PUBLIC_KEY, write-back to local SQLite, cache. - Local SQLite fallback — preserved; only reached when Supabase is unreachable.
- Cache (
/api/license/currentresponse now includes asourcefield:supabase | local | cache | none. The tenant UI subscribes viaclient/src/store/license.tsand renders a banner mapping each source to a tone (Supabase = emerald, local = amber, none = rose).- RLS policy for
tenant_licenses: seeserver/src/database/migrations/knex/2026072900000000_tenant_licenses_rls_read_policy.jsand the equivalentsql/20260729_tenant_licenses_rls_read_policy.sql. The policy allows theanonandauthenticatedroles to SELECT; the application layer scopes every fetch bytenant_id. No INSERT/UPDATE/DELETE policy is granted to anon, so tenants cannot write. - Graceful degradation: every response carries
X-License-Source: supabase | local | cache | nonefor observability. Fallback to local on Supabase 5xx / network errors /SUPABASE_AUTH401. The X-License-Status header continues to reportACTIVE | EXPIRED | No-License | Invalid-Signature | …. - UI surface:
- SuperAdmin
Cloud Linkpage gains aTenant License Distribution Flowsection explaining the three steps. - SuperAdmin
Licensing Enginepage has aDELIVERED VIA SUPABASEchip and a delivery-channels note in the Trust Anchor section. - Tenant client renders
LicenseSourceBannermounted under the nav, surfacing the source (Supabase / local cache) and term status.
- SuperAdmin
3. Dynamic Multi-School Instance Spawner & Provisioning
- Template Instance: The current
server/(port 3001) andclient/(port 3000) represent the template school instance in dev mode (the Vite dev server proxies/apitohttp://127.0.0.1:3001). - Instance Spawner (
InstanceSpawner.js):- Located in
superadmin/server/src/services/InstanceSpawner.js. - Spawns isolated node server & client child processes for each subscribing school.
- Provisions an isolated database file
server/data/schools/<school_id>/school.db(or dedicated PostgreSQL database schema per tenant). - Dynamically assigns unique server ports (
3010,3011,3012...) and client ports (5180,5181...) (per-instance vite dev server inherits its target ports from the orchestrator and reads its/apiproxy target from the spawned node backend's port via theVITE_API_TARGETandVITE_WS_TARGETenv vars passed at spawn time). - Generates and injects cryptographically signed termly license tokens (
LICENSE_KEY). - Persists the spawned-instance registry, port counter, and last 500 stdout/stderr lines per instance to a sidecar SQLite DB at
superadmin/server/data/spawner-registry.dbso the orchestrator survives restarts. - Monitors child process PIDs, streams stdout/stderr telemetry, and exposes remote start / stop / restart controls from the SuperAdmin Portal (registered in the orphaned state on startup so previously-spawned instances can be re-launched without re-onboarding).
- Located in
4. Sprint Breakdown & Implementation Details
🚀 Sprint 1: Multi-Tenant Data Architecture
- PostgreSQL Migration DDL: Migrated database schema definitions from SQLite constructs to PostgreSQL compatible Knex DDL (
SERIAL/BIGSERIALprimary keys,CURRENT_TIMESTAMP, Postgres enum/check constraints). - Tenant Isolation: Injected
tenant_idcolumn across all domain tables (users,classes,subjects,enrollments,courses,payments,grades,attendance,assignments,submissions,inventory,payroll,exams,hostels,transport,clubs, etc.). - Row Level Security (RLS): Enforced PostgreSQL RLS policies on all domain tables:
CREATE POLICY tenant_isolation_policy ON <table> FOR ALL USING ( tenant_id = current_setting('app.current_tenant', true) OR current_setting('app.is_super_admin', true) = 'true' );
💳 Sprint 2: Super Admin & Licensing Engine (Backend)
- Tenants & Licensing Schema:
tenants: Primary tenant registry storingid,name,code,contact_email,phone,status(active,suspended,trial,grace_period).tenant_licenses: Termly license registry storinglicense_key,term_name,academic_year,start_date,end_date,grace_period_days,max_students,max_staff,status(active,expired,revoked,overridden).
- Cryptographic License Token Generator (
LicenseTokenGenerator.js): Generates cryptographically signed HMAC-SHA256 (HS256) JWT tokens (LIC-TERM1-2026-TOKEN...) containing tenant metadata, validity windows, and learner caps to enable offline verification on local PWA/school servers. - Licensing Access Middleware (
licensingMiddleware.js):- Inspects
X-Tenant-ID/X-Tenant-Codeheader or JWT payload. - Validates active term license dates + grace period.
- If a school's license is expired, allows
GETread-only operations for data retrieval and billing navigation, but strictly blocks all non-readonly endpoints (POST,PUT,PATCH,DELETE) with402 Payment Required.
- Inspects
🖥️ Sprint 3: Super Admin Portal (Frontend & API)
- Standalone Location:
e:\nextgen\next-gen\superadmin\- Server:
superadmin/serverrunning on Port 3002 with isolated master key/JWT authentication (X-SuperAdmin-Key). - Client:
superadmin/clientrunning on Port 3003 built with Vite, React, Lucide Icons, and Tailwind CSS.
- Server:
- Key Features:
- National Ecosystem Telemetry: Real-time aggregated stats (Subscribing Schools Count, Active vs Expired Licenses, Total Active Learners, Termly Revenue (rolling 90-day SUM of completed
payments.amount, returned in USD by convention)). - School Onboarding Drawer: Form to register new subscribing institutions and issue initial term licenses.
- Termly License Generator: Tool to generate signed term licenses (
Term 1,Term 2,Term 3,Annual) with custom validity dates. - Emergency Access Override: Form to grant emergency extensions with rationale audit logs.
- National Ecosystem Telemetry: Real-time aggregated stats (Subscribing Schools Count, Active vs Expired Licenses, Total Active Learners, Termly Revenue (rolling 90-day SUM of completed
🔄 Sprint 4: Sync Engine Event Sourcing Upgrade
- Idempotent Event Processor (
SyncEngine.js):- Assigns/verifies UUIDv4
event_idand monotonic sequence numbers (seq_num) for every pushed edit event. - Maintains
sync_eventstable to reject duplicate events.
- Assigns/verifies UUIDv4
- UTC Clock Normalization:
- Calculates time delta between client connection timestamp and server UTC time (
server_now - client_time). - Converts client timestamps to normalized UTC server time and rejects stale sequence events (
seq_num <= max_seq).
- Calculates time delta between client connection timestamp and server UTC time (
5. API Reference Guide
SuperAdmin Gateway API (Port 3002)
| Method | Endpoint | Description | Auth Header |
|---|---|---|---|
POST |
/api/auth/login |
SuperAdmin Master Login | None |
GET |
/api/health |
Liveness check (no auth) | None |
GET |
/api/metrics |
Global Ecosystem Health & Licensing Telemetry | X-SuperAdmin-Key |
GET |
/api/tenants |
List all subscribing schools with license status | X-SuperAdmin-Key |
POST |
/api/tenants |
Onboard a new school tenant & issue license | X-SuperAdmin-Key |
POST |
/api/tenants/:id/licenses |
Issue / renew a signed termly license | X-SuperAdmin-Key |
POST |
/api/tenants/:id/override |
Grant emergency access override | X-SuperAdmin-Key |
GET |
/api/instances |
List all spawned school server/client instances | X-SuperAdmin-Key |
POST |
/api/instances/spawn |
Spawn a new school server & client instance | X-SuperAdmin-Key |
POST |
/api/instances/:id/stop |
Stop a running school instance | X-SuperAdmin-Key |
POST |
/api/instances/:id/start |
Start a previously-spawned stopped school instance | X-SuperAdmin-Key |
POST |
/api/instances/:id/restart |
Restart a running or stopped school instance | X-SuperAdmin-Key |
GET |
/api/instances/:id/logs |
Fetch live stdout/stderr logs of a school instance | X-SuperAdmin-Key |
6. SuperAdmin UI Component Architecture, Design System & Theme Adaptation
The SuperAdmin Portal UI (superadmin/client) is built to match the exact design system, UI components, typography, layout, and light/dark theme adaptation of the main Nextgen LMS school application (client/src).
1. Unified Modal Component Architecture (components/ui/Modal.tsx)
- Single Component Pattern: All SuperAdmin modal windows (School Onboarding, Term License Generation, Emergency Access Override, Instance Spawning, and School Profile Inspector) use the single
Modalcomponent copied fromclient/src/components/ui/Modal.tsx. - Blueprint Compliance (
modals.md):- Portal Rendering: Uses React
createPortalto mount modal elements directly todocument.body. - Backdrop Styling: Deep Navy blur backdrop
bg-[#002147]/60 backdrop-blur-smwith defaultzIndex = 150. - Border Radius: Custom
rounded-[2.5rem]corner styling matching application design guidelines. - Watermark & Header:
ModalHeaderincorporates rotating background watermark icons (DisplayWatermark), title, uppercase tracking subtitle, and close button (X). - Scroll Locking: Automatically locks body background scroll upon open and unlocks on close.
- Portal Rendering: Uses React
2. School Tenant Profile Inspector Modal (components/TenantProfileModal.tsx)
- Institution Overview: Displays full school metadata (name, code, contact email, phone, tenant ID).
- Subscribed Capacity Progress Meters: Interactive progress bars displaying enrolled student count vs max student cap, and staff directory count vs max staff cap.
- Cryptographic Token Viewer: Displays active signed HS256 license token (
LIC-TERM1-2026...) with a one-click Copy to Clipboard button. - Direct Action Buttons: Quick triggers to Renew Term License, Grant Emergency Access Override, or Spawn Isolated Server Instance.
3. Live Telemetry & Server Logs Terminal Stream (components/TelemetryLogsView.tsx)
- Terminal Styling: Styled as a high-tech terminal window (
#050811) with live streaming telemetry output. - Level Filtering: Filter log stream by level (
ALL,INFO,WARN,ERROR). - Log Search & Export: Full-text log searching and one-click log file download (
.log). - Source: the orchestrator's persisted log buffer (
/api/instances/:id/logs); the view auto-refreshes every 3s when the user has selected a spawned instance from the dropdown.
4. Toast Notification System (components/ui/Toast.tsx)
- Floating feedback notifications in bottom-right corner providing user feedback upon school onboarding, key generation, emergency access overrides, or instance spawner actions.
5. Multi-Filter Toolbar & Registry CSV Export
- Search box filtering school name, code, or contact email.
- Status dropdown filter (
ALL,ACTIVE,EXPIRED,OVERRIDDEN). - One-click CSV export button (
schools_directory_YYYY-MM-DD.csv) (filter values mirror the documentedtenant_licenses.statusenum:active,expired,overridden,revoked,grace_period,trial).
7. Development & Testing Considerations for Local Instances
When developing and testing the application locally using the template school instance (server/ on Port 3001, client/ on Port 3000, with /api proxied to http://127.0.0.1:3001 by Vite) alongside SuperAdmin (superadmin/server on Port 3002, superadmin/client on Port 3003), take care of the following key items:
1. Dev Mode Default Tenant Seeding & Fallback
- Default Seeding: Running
npm run db:initautomatically executes migration2026072200000000_multitenant_licensing_rls.js, creating the5b39bcb5-506a-456d-b7cb-91730b397cc1record intenantsand seeding a 1-year valid active term license (LIC-DEFAULT-2026-TERM1-VALID). - Seamless Local Dev Testing: If
TENANT_IDis not explicitly passed in environment variables or HTTP headers,licensingMiddlewaredefaults totenant_id = 5b39bcb5-506a-456d-b7cb-91730b397cc1. This ensures local developers can build and test UI features without being blocked by licensing checks.
2. Dual-Database Engine Switch (SQLite vs PostgreSQL RLS)
- Local SQLite Mode (default and only currently working path):
server/runs onbetter-sqlite3(data/school.db). The PostgreSQL RLS branch of the multi-tenant migration is gated byknex.client.config.client === 'pg'(seeserver/src/database/migrations/knex/2026072200000000_multitenant_licensing_rls.js:77) and is bypassed whenknexfile.jsresolves to SQLite. Tenant isolation is still enforced, just at the application layer vialicensingMiddlewareandcurrent_setting-style SQL filter parameters in controllers. - PostgreSQL RLS path (currently requires code change): The migration file contains the full
ALTER TABLE ... ENABLE ROW LEVEL SECURITYand policy DDL shown in Section 4 / Sprint 1, butserver/knexfile.js:10hardcodesclient: 'better-sqlite3'and ignores theDB_CLIENT/DATABASE_URLenvironment variables. To exercise the native RLS branch, editserver/knexfile.jsto honorprocess.env.DB_CLIENTand provide a Postgres connection, then re-runnpm run db:init.
3. Testing Expired Term Licenses & HTTP 402 Restrictive Mode
- Simulating Expired License: To test how the application behaves when a school's license expires:
- Open SQLite database (
server/data/school.db) or execute SQL:UPDATE tenant_licenses SET end_date = '2025-01-01', status = 'expired' WHERE tenant_id = '5b39bcb5-506a-456d-b7cb-91730b397cc1'; - Perform a
GETrequest (e.g.GET /api/students): Notice response succeeds with headerX-License-Restriction: Read-Only Mode Enforced. - Perform a
POSTorPUTwrite request (e.g.POST /api/grades): Notice response is blocked with HTTP status402 Payment Requiredand JSON payload:{ "error": "Payment Required / Term License Expired", "code": "LICENSE_EXPIRED", "message": "School term license has expired. Non-readonly operations are restricted.", "billing_url": "/billing", "tenant_id": "5b39bcb5-506a-456d-b7cb-91730b397cc1" }
- Open SQLite database (
4. Testing Emergency Access Overrides
- To test granting an emergency override to an expired tenant:
- Make a request from SuperAdmin UI or API:
curl -X POST http://localhost:3002/api/tenants/5b39bcb5-506a-456d-b7cb-91730b397cc1/override \ -H "X-SuperAdmin-Key: SA-MASTER-KEY-2026-X99" \ -H "Content-Type: application/json" \ -d '{"reason": "Ministry emergency extension", "extension_days": 14}' - Verify that write access (
POST,PUT) is immediately restored for the tenant.
- Make a request from SuperAdmin UI or API:
5. Testing Multi-School Instance Spawning & Port Management
- Instance Spawner: Test spawning new school instances via the SuperAdmin UI or calling
POST /api/instances/spawn. - Directory Isolation: Each spawned school instance creates its own isolated database in
server/data/schools/<school_id>/school.dband is assigned a unique server port (e.g.3010,3011). - Child Process Execution: Ensure
nodeis available in system PATH soInstanceSpawnercan executenode src/index.jscleanly.
6. Cloud-First License Fetch (Supabase direct read)
The new flow is testable end-to-end on dev:
-
From the SuperAdmin UI, issue a fresh term license to a tenant.
-
Verify the response headers from any tenant API call:
curl -i http://localhost:3001/api/students -H "Authorization: Bearer <jwt>" \ -H "x-tenant-id: 5b39bcb5-506a-456d-b7cb-91730b397cc1" | grep -i x-license # X-License-Status: ACTIVE # X-License-Source: supabase -
Disable network egress to Supabase and re-issue the same request. The middleware should fall back to local SQLite and set
X-License-Source: localwhile still allowing the mutation (the previous license was write-backed on the first successful fetch). -
The vitest suite
server/tests/supabase-license-fetch.test.jsexercises the middleware's Supabase path with a stubbedSupabaseLicenseServicemodule. Run it with:cd server && npm test -- supabase-license-fetch -
The RLS policy must be applied to the Supabase project itself for cloud reads to succeed. Apply with:
psql "$SUPABASE_DB_URL" -f sql/20260729_tenant_licenses_rls_read_policy.sqlor paste the SQL into the Supabase SQL editor. The Knex migration
2026072900000000_tenant_licenses_rls_read_policy.jsis a no-op on SQLite and runs the policy on Postgres.
7. Testing Sprint 4 Sync Engine Event Sourcing & Clock Normalization
- Idempotent Push Testing:
- Pushing an edit with
event_id = "evt-12345"inserts a record insync_events. - Re-sending the exact same
event_idreturns{ isDuplicate: true }without running duplicate SQL writes.
- Pushing an edit with
- Clock Drift Testing:
normalizeClientTimestamp()calculates server-client time delta. Sending an offline record with a pastclient_timestampnormalizes the timestamp to server UTC time and rejects sequence numbers older thanmax_seq.
8. Port & Service Architecture Summary
| Service Component | Directory Path | Listening Port | Technology Stack |
|---|---|---|---|
| School Tenant Server (Template) | server/ |
3001 |
Node.js / Express / SQLite / PostgreSQL |
| School Tenant Client (Template) | client/ |
3000 |
React / Vite / Tailwind CSS |
| SuperAdmin Server | superadmin/server/ |
3002 |
Node.js / Express / Supabase SDK / HS256 Crypto JWT Engine |
| SuperAdmin Portal | superadmin/client/ |
3003 |
React / Vite / Tailwind CSS |
| Spawned School Instances | server/data/schools/<id> |
(3010+, ports and registry persisted to spawner-registry.db) |
Node.js / Express / Dynamic Port Allocation |