ArchitectureOverview

Architecture Overview

One-line: a single multi-tenant Flask API serves many schools, fronted by two Next.js web apps and one Expo mobile app, with PostgreSQL, Redis, and Celery behind it.

The shape of the system

                         ┌─────────────────────────────┐
   School staff ───────► │  admin-web (Next.js)         │
   (browser)             │  school dashboard, tenant-only│
                         └──────────────┬──────────────┘
                                        │  HTTPS  (X-Tenant-ID / subdomain + JWT)
   Teachers/Students ──► ┌──────────────▼──────────────┐
   (Expo mobile app)     │      Flask REST API          │
                         │  server/  — /api/*           │
   NexSchool operators ► │  blueprints per module       │
   (browser)             └──────┬───────────┬───────────┘
        │                       │           │
        │ panel (Next.js)  ┌────▼────┐  ┌───▼────┐  ┌──────────┐
        └─────────────────►│Postgres │  │ Redis  │  │  Celery  │
          /api/platform/*  │ (tenant │  │ cache +│  │ workers  │
                           │  data)  │  │ broker │  │ (async)  │
                           └─────────┘  └────────┘  └──────────┘

Components

ComponentRepo / pathRole
Flask APInexchool/server (server/)All business logic, REST API under /api/*, app factory in server/app.py, one blueprint per module in server/modules/<name>/
admin-webnexchool/school-admin-panel (admin-web/)Next.js dashboard used inside a school (tenant-scoped)
panelnexchool/super-admin-panel (panel/)Next.js control plane for NexSchool operators (/api/platform/*)
mobile clientnexchool/client (client/)Expo React Native app for teachers, students, admins
infranexchool/nexchool-infraDocker Compose, Nginx, CI/CD, deploy scripts
landing-pagenexchool/landing-pageMarketing site (nexchool.in), Vercel-hosted, separate from the app

Layers (inside the API)

  • HTTP — Flask blueprints registered in server/app.py (register_blueprints). One module = one blueprint under /api/<module>.
  • Business logic — service layer per module (server/modules/<name>/services.py). Controllers never touch the DB directly.
  • Data — SQLAlchemy 2 models. Tenant-scoped rows inherit TenantBaseModel and carry tenant_id; scoping is enforced in core/database.py (see Multi-tenancy).
  • AuthZ — permission strings shaped resource.action, enforced by decorators (auth_required, tenant_required, require_permission, require_plan_feature). See Auth & RBAC.
  • Async — Celery workers with Redis as broker for background jobs (imports, sweeps, notifications).

Request lifecycle (tenant request)

1. Request hits Nginx → Flask.
2. Tenant resolved (subdomain in Host, or X-Tenant-ID header, or auth-body field).
3. JWT verified; user + roles loaded (permissions cache-backed via Redis).
4. Decorators check: authenticated? tenant set? has permission? plan feature enabled?
5. Controller → service → SQLAlchemy (auto-filtered by tenant_id).
6. Response wrapped in a consistent { success, data } envelope.
7. Cache-Control: no-store + Vary headers set so no tenant's response is reused for another.

Platform requests (/api/platform/*) skip tenant resolution and require a platform-admin JWT instead.

Frontends unwrap one envelope

Every client (admin-web/src/services/api.ts, the panel equivalent, client/common/services/api.ts) speaks to the same API and unwraps the same { success, data } envelope. fetch uses cache: "no-store" so the browser HTTP cache can never serve one tenant’s data to another.