Multi-tenancy: one database, schema per school
Decision: Use django-tenants with a PostgreSQL schema per tenant and a public schema for shared data. Why:- Strong isolation — School A’s data is not queryable from school B’s schema by accident; boundaries are enforced at the database layer, not only in application code.
- Operational simplicity — One database cluster to backup and migrate;
migrate_schemasapplies tenant migrations across all schools. - Fits PostgreSQL — Schema-based tenancy is a well-understood pattern on Postgres.
Two API surfaces: system vs school
Decision: Split HTTP APIs under/api/v1/system/ (platform) and /api/v1/school/ (tenant operations).
Why:
- Clear security story — System endpoints assume public schema and platform roles; school endpoints assume tenant schema and school RBAC.
- Predictable URLs — Operators and generated clients can distinguish governance from day-to-day school work.
- Aligns with middleware —
SmartTenantMiddlewarekeeps system (and docs) routes on public while resolving tenant context for school traffic.
Tenant resolution in middleware
Decision: CustomSmartTenantMiddleware (extends django-tenants) resolves schema using, in order: global path list (stay on public), then JWT (user’s school), X-Schema-Name (superadmin), then hostname → Domain → Client.
Why:
- JWT-first for APIs — Mobile and SPA clients often hit a single host; the token carries identity, and the user’s linked client determines the schema.
X-Schema-Namefor support — Superadmins can target a school without juggling DNS.- Host-based fallback — Matches classic subdomain-per-school deployments.
/api/v1/chatbot/. Tenant context for those requests follows the same middleware rules as other non-system API traffic (host, JWT, X-Schema-Name); individual views may also resolve a school using phone numbers or payloads where the product requires it.
Django REST Framework with ViewSets and routers
Decision: Expose the API primarily through DRFModelViewSets registered on DefaultRouter and NestedDefaultRouter.
Why:
- Consistency — List/create/retrieve/update/delete patterns repeat across many resources.
- Nested URLs — Academic structure is naturally hierarchical (session → academic class → registration; session → exam session → exam → marks).
- Less boilerplate than hand-written function views for CRUD-heavy domains.
Permissions: Django model permissions by HTTP method
Decision: Default permission classDynamicModelPermissions maps GET/POST/PUT/PATCH/DELETE to view_ / add_ / change_ / delete_ model permissions.
Why:
- Single source of truth — Authorization reuses Django’s permission tables and school roles.
- Auditable — Permissions are data, not scattered
ifstatements in every view.
Authentication: JWT plus optional API keys
Decision: Simple JWT for user sessions (Bearer tokens); platform-managed API keys for integrations (documented in the API reference).
Why:
- Stateless API — Fits mobile apps and third-party servers.
- Separation — Human flows use login/refresh; automation can use scoped keys where supported.
sms/settings.py under SIMPLE_JWT.
OpenAPI and documentation
Decision: drf-spectacular generates OpenAPI 3; the repo commitsdocs/api-reference/openapi.json for Mintlify; live servers expose Swagger/ReDoc at /api/docs/ and /api/redoc/.
Why:
- Contract for integrators — A single schema drives Mintlify’s API tab and can drive client generation.
- Hooks for quality —
PREPROCESSING_HOOKS/POSTPROCESSING_HOOKStrim or annotate the schema (for example tenant header, tag ordering) so published docs stay readable.
manage.py spectacular; treat regeneration as part of the API change workflow.
Exception handling and error shape
Decision:global_exception_handler wraps DRF’s handler, delegates to core where needed, and attaches status_code to JSON responses when the payload is a dict.
Why:
- Consistent client parsing — Callers can rely on HTTP status and a structured body.
- Swagger UX — Special handling for docs routes triggers browser-friendly 401 + WWW-Authenticate for protected Swagger.
Async work: Celery and Redis
Decision: Celery with Redis (when configured) for background work such as chatbot pipelines and report generation. Why:- Non-blocking HTTP — Long-running PDFs or messaging should not tie up request workers.
- Operational flexibility — Workers scale independently of the web tier.
Static files and Swagger assets
Decision: Use drf-spectacular-sidecar and WhiteNoise so Swagger/ReDoc assets ship reliably in production. Why:- No CDN dependency for core API docs in locked-down environments.
Audit logging
Decision:AuditMiddleware (after authentication) records sensitive actions for compliance and debugging.
Why:
- Accountability — School operations often require “who changed what and when.”
Related reading
- Repository structure — file and app layout
- Architecture — request flow and API map
- API Reference — how to call the API and refresh the schema