Skip to content

Architecture

1. Overview and principles

SIS is a multi-tenant Student Information System for K–12 schools. The backend is a modular NestJS monolith: domain modules own business rules and expose explicit service or query contracts to consumers. Shared infrastructure handles authentication, authorization, transactions, validation, errors and external transports.

The design favors a small number of reusable patterns:

  • TypeScript services and DTOs with generated Prisma types at the persistence boundary.
  • Tenant isolation in both service predicates and PostgreSQL row-level security.
  • Entity–Scope–Action permissions, supplemented by record policies and qualified role assignments.
  • Domain-owned query functions instead of a generic repository layer.
  • Explicit temporal history where school records need a dated answer.
  • Atomic database changes, followed by external work through the post-commit coordinator or a durable outbox.

Module boundaries help contain change; they do not make service extraction cost-free. Shared transactions and dependencies must be considered before changing a deployment boundary.

2. Components

Component Implementation Responsibility
HTTP application NestJS, Express, Passport and JWT Routing, authentication, validation and response projection
Persistence Prisma with PostgreSQL adapter Typed queries and transactions
Isolation Tenant predicates, transaction-local GUC and PostgreSQL RLS Restrict tenant-owned data
Authorization Guards, record policies, field filtering Restrict operations, rows and fields
Background work Nest scheduling and database-backed delivery/run state Notifications, credential delivery, communications and reports
Files FileStoragePort with S3-compatible transport MinIO locally and managed buckets in hosted environments
Email and push Mailer and notification transport interfaces Provider-independent delivery
Backoffice Embedded Vite/React application Platform administration through the API
Timetable solver Python service behind SolverPort Generation from a backend-owned snapshot

The application does not depend on Redis or BullMQ. Scheduled jobs register through ScheduleModule/SchedulerRegistry; durability and claiming belong to the relevant database-backed producer. HTTP uploads go through backend file services; authorized reads return signed storage URLs.

The school frontend is a separate application. Its hosting and UI choices do not define backend contracts. Deployment configuration for this repository lives in railway.toml; see infrastructure.

3. Request and transaction boundaries

src/app.module.ts composes global origin/rate-limit guards and TenantTransactionInterceptor. @ProtectedResource() adds JWT, scope, action, field-write and role guards plus response field filtering.

Guards run before the request transaction. Tenant reads performed there need the explicit withTenantGuc helper. The interceptor sets the tenant GUC and routes Prisma operations onto one ambient connection; a nested service $transaction joins that transaction rather than committing independently.

PostCommitCoordinator owns deferred request-side effects. Register work while its scope is active; do not flush a service-local queue before the outer request commits. Background jobs and exempt routes establish their own explicit tenant/transaction boundaries. Long external operations must not hold a database transaction open. See multitenancy and maintainability.

4. Data access and module ownership

Services coordinate business rules; <domain>.queries.ts and focused query satellites own includes, selects and database operations. Query functions accept the relevant Prisma client or transaction so callers retain transaction ownership. BaseTenantedCrudService supplies the shared person CRUD lifecycle; specialized aggregates use their own services.

Keep tenant predicates even with RLS. RLS prevents cross-tenant access; it does not enforce academic-year selection, caller row authority or field permissions. Foreign identifiers must be resolved under the correct tenant and domain scope before use.

Cross-module reads should use the owner's public query/service contract. Avoid copying another module's relationship graph into a consumer. A complex orchestration belongs in a focused domain service; it does not require a repository abstraction across the codebase. See CRUD patterns and typing conventions.

5. Identity and academic-year scoping

LoginIdentity owns shared login credentials; User is a school membership. Teacher, Staff and Student rows are academic-year snapshots linked across years by personUuid. Referents are tenant-level contacts linked to students. These identities serve different purposes and must not be substituted for each other.

Reads that support historical years accept academicYearId and otherwise resolve the active year. Ordinary person creation resolves the active year itself. Mutation gates belong to the domain: the CRUD base rejects archived-year writes, while lifecycle and correction endpoints can impose more specific rules. Do not infer write authority from a read-side year selector.

Duplicate detection is scoped to the target year. Import classification uses identity matching separately from email/account checks; cross-year carry uses resolvePriorYearPerson and only carries a bound account when its email agrees. See authentication and imports.

Catalogues and global records have different scope rules. School, User, Role and Referent are not person-year snapshots; global preset rows may have a nullable tenant. Consult the model and owning service before adding a blanket year filter.

6. Time and retained records

School-local dates come from the shared school clock. Effective-dated placement, selection, teaching and timetable history answer which facts govern a date or lesson boundary; mutable head columns describe forward state and cannot replace that history.

Attendance snapshots, grade revisions and the audit log have distinct retention and write contracts. Use each domain's invariants instead of applying generic CRUD deletion or recalculation to historical evidence. See class organization, timetables, attendance, audit and grades.