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
| Component | Repo / path | Role |
|---|---|---|
| Flask API | nexchool/server (server/) | All business logic, REST API under /api/*, app factory in server/app.py, one blueprint per module in server/modules/<name>/ |
| admin-web | nexchool/school-admin-panel (admin-web/) | Next.js dashboard used inside a school (tenant-scoped) |
| panel | nexchool/super-admin-panel (panel/) | Next.js control plane for NexSchool operators (/api/platform/*) |
| mobile client | nexchool/client (client/) | Expo React Native app for teachers, students, admins |
| infra | nexchool/nexchool-infra | Docker Compose, Nginx, CI/CD, deploy scripts |
| landing-page | nexchool/landing-page | Marketing 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
TenantBaseModeland carrytenant_id; scoping is enforced incore/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.
Related
- Services & repos — each repo in detail
- Auth & RBAC
- Multi-tenancy
- Control plane
- Infrastructure & deploy