Architecture Overview
SA3 is a containerised Next.js 15 application deployed on AWS App Runner, backed by RDS PostgreSQL 15, AWS S3 for file storage, CloudFront for public assets, and AWS Lambda for batch PDF generation. All infrastructure is provisioned via Terraform and runs in AWS eu-west-3 (Paris).
Goals
- Single-school deployment -- not multi-tenant SaaS. Simplicity and cost-efficiency over scale-out architecture.
- Multiple academic systems -- GES Primary, WASSCE, Cambridge IGCSE in one deployment.
- Offline capability -- teachers enter scores with variable internet connectivity; offline-first score entry with background sync.
- Batch PDF generation -- 500--1500 report cards per term, generated via Lambda fan-out.
- Data privacy -- student PII encrypted at the application layer (AES-256-GCM via KMS), compliant with Ghana Data Protection Act 2012.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router, React 19, Turbopack) |
| Database | RDS PostgreSQL 15 (eu-west-3, private VPC) |
| ORM | Prisma 5 |
| Auth | next-auth v4 (CredentialsProvider) |
| i18n | next-intl |
@react-pdf/renderer via Lambda | |
| File storage | AWS S3, CloudFront (assets), pre-signed URLs (PII) |
| Deployment | AWS App Runner (containerised) |
| AWS SES | |
| Offline | next-pwa (Workbox), IndexedDB for score queue |
| IaC | Terraform |
Infrastructure Diagram
Dual Portal Architecture
SA3 has two distinct portals in one Next.js app:
| Portal | URL prefix | Roles | Purpose |
|---|---|---|---|
| System Admin | /admin/** | ADMIN only | Academic configuration, user management, report generation |
| Staff Portal | /app/** | All authenticated staff | Score entry, class views, report viewing |
Both portals share the same API layer (/api/admin/**). The /api/admin/ prefix is a naming artifact -- all handlers enforce access via resolvePermissionsForResource for data-level access control.
See ADR-006: Dual Portal Architecture for the full decision record.
Data Model Overview
The Prisma schema contains 28 entities organised in a build-order dependency chain:
| Phase | Models |
|---|---|
| 1 | AcademicSystem, SchoolSection, YearGroup, AcademicYear, AcademicPeriod, GradeScale, GradeBoundary, AssessmentType, WeightingRule, Subject, SubjectDocument |
| 2 | Student, Staff, Role, StaffRole, Department, StaffDepartment |
| 3 | Class, ClassStudent, ClassTeacher, ClassSubject, ClassMaterial |
| 4 | Assessment, AssessmentScore, AssessmentAuditLog |
| 5 | ReportGroup, GeneratedReport, GeneratedReportScore, TeacherRemark, ReportGroupAuditLog |
Key Design Constraints
- Permission checks are mandatory. Every API route calls
resolvePermissionsForResource-- there is no shortcut. - PII encryption is explicit.
encryptField/decryptFieldcalled in service functions, never in Prisma middleware. - StaffRole is append-only. Revocations set
revokedAt; rows are never deleted. - AssessmentAuditLog is append-only. Every score change produces a new audit log row.
- GeneratedReportScore is write-once. On regeneration, delete + insert in a transaction.
- One PDF per Lambda invocation.
renderToBuffer()is never called in a loop due to@react-pdf/renderermemory leak.