Skip to main content

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

LayerTechnology
FrameworkNext.js 15 (App Router, React 19, Turbopack)
DatabaseRDS PostgreSQL 15 (eu-west-3, private VPC)
ORMPrisma 5
Authnext-auth v4 (CredentialsProvider)
i18nnext-intl
PDF@react-pdf/renderer via Lambda
File storageAWS S3, CloudFront (assets), pre-signed URLs (PII)
DeploymentAWS App Runner (containerised)
EmailAWS SES
Offlinenext-pwa (Workbox), IndexedDB for score queue
IaCTerraform

Infrastructure Diagram

Dual Portal Architecture

SA3 has two distinct portals in one Next.js app:

PortalURL prefixRolesPurpose
System Admin/admin/**ADMIN onlyAcademic configuration, user management, report generation
Staff Portal/app/**All authenticated staffScore 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:

PhaseModels
1AcademicSystem, SchoolSection, YearGroup, AcademicYear, AcademicPeriod, GradeScale, GradeBoundary, AssessmentType, WeightingRule, Subject, SubjectDocument
2Student, Staff, Role, StaffRole, Department, StaffDepartment
3Class, ClassStudent, ClassTeacher, ClassSubject, ClassMaterial
4Assessment, AssessmentScore, AssessmentAuditLog
5ReportGroup, GeneratedReport, GeneratedReportScore, TeacherRemark, ReportGroupAuditLog

Key Design Constraints

  1. Permission checks are mandatory. Every API route calls resolvePermissionsForResource -- there is no shortcut.
  2. PII encryption is explicit. encryptField/decryptField called in service functions, never in Prisma middleware.
  3. StaffRole is append-only. Revocations set revokedAt; rows are never deleted.
  4. AssessmentAuditLog is append-only. Every score change produces a new audit log row.
  5. GeneratedReportScore is write-once. On regeneration, delete + insert in a transaction.
  6. One PDF per Lambda invocation. renderToBuffer() is never called in a loop due to @react-pdf/renderer memory leak.