# Backend Architecture

The backend uses a three-layer architecture, mirroring the frontend
(`frontend/docs/frontend-architecture.md`):

- API layer (HTTP)
- Business logic layer (BLL)
- Data access layer (DAL)

The goal is to keep routes thin, keep business rules testable and free of HTTP,
and keep all persistence in one place.

## Layer 1: API

Location:

- `src/routes/` — thin route wiring: `path → middleware → wrapAsync(controller)`.
- `src/api/controllers/` — one `<feature>.controller.ts` per feature; exported
  async handler functions `(req, res) => …`.
- `src/api/http/` — request helpers (`wrapAsync`, `queryStr`, `queryNum`,
  `paramStr`).
- `src/middlewares/` — `authenticate` (passport), `checkPermissions`,
  `csrf-origin`, `error-handler`, `upload`.

Responsibilities:

- Parse and validate the HTTP request (query, params, body, cookies, uploads).
- Run middleware (auth, permissions, CSRF).
- Call exactly one BLL service and shape the HTTP response (status + body).
- Own multipart/upload parsing; pass parsed data (e.g. a file buffer) to the BLL.

The API layer must not:

- Import the DAL (`@/db/api/*`, `@/db/models/*`) — it goes through a service.
- Contain tenant/role/permission/workflow rules or DTO mapping.
- Run database queries.

### Authentication and public routes

Every `/api` route is JWT-authenticated at the mount (`authenticated = passport.authenticate('jwt', { session: false })`) **except** the intentionally public surface:

- the `/api/auth/*` public endpoints (sign-in / refresh / sign-out, password reset, email verification, OAuth — the authenticated sub-routes such as `/me` apply passport per route);
- `GET /api/public/campuses`;
- `GET /api/public/content-catalog/:contentType`.

No tenant-owned mutable data is exposed publicly. Authorization is then by permission: generic-CRUD routers apply `checkCrudPermissions(entity)` (`${METHOD}_${ENTITY}`); the feature routes apply `checkPermissions(<PRODUCT_FEATURE>)` for page reads and the special actions (`READ_FRAME`, `READ_WALKTHROUGH`, `READ_ATTENDANCE`, `READ_PARENT_COMM`, `READ_INTERNAL_COMM`, `FILL_ATTENDANCE`, `TAKE_QUIZ` — names from `shared/constants/product-permissions.ts`, so `custom_permissions` can extend access), while the manager-only writes (FRAME/walkthrough/communications/content-catalog editing and the staff/attendance reports) stay gated in their services by role until a dedicated `MANAGE_*` permission exists. The `users` / `staff` / `organizations` write paths add the §3.3 relational policy. Both `POST /api/file/upload` and `GET /api/file/download` require JWT, the local file handlers reject path traversal, and download enforces a per-file tenant/ownership check (the file's owning organization must match the requester's unless they have global access; see `file.md`).

## Layer 2: Business Logic (BLL)

Location:

- `src/services/` — one `<feature>.ts` (class with static methods) per feature,
  plus per-feature mappers/validators/helpers as needed. Infra BLL lives in
  `src/services/email/`.

Responsibilities:

- Own workflows, transactions, and coordination across repositories.
- Apply tenant, role, campus, and permission rules.
- Map DB records to response DTOs; validate and normalize inputs.
- Accept typed inputs and return typed values/DTOs.

The BLL must not:

- Touch Express `req`/`res` or import `express`/middleware. (Two legacy
  exceptions remain — `services/file.ts` streaming and `services/auth.ts` session
  IP/UA + cookies — tracked by the boundary test and to be revisited.)
- Import the API layer.
- Render HTTP responses.

## Layer 3: Data Access (DAL)

Location:

- `src/db/api/` — one `*DBApi` class per entity (the repository layer).
- `src/db/models/` — Sequelize models.
- `src/db/migrations/`, `src/db/seeders/`, `src/db/utils.ts`, `db.config.ts`.
- `src/db/api/types.ts` — DB-entity contract types (`AuthenticatedUser`,
  `CurrentUser`, `DbApiOptions`, …); DAL-coupled, so it stays in `db/`.

Responsibilities:

- Own all Sequelize queries and schema.
- Return records/plain data to the BLL.

The DAL must not:

- Import the API layer or the BLL. (One legacy exception: `db/api/file.ts`
  imports `services/file` for GCloud blob deletion — tracked, to be inverted.)
- Apply business rules or touch HTTP.

## Cross-cutting

Location: `src/shared/` (+ ambient types in `src/types/`).

- `shared/constants/` — all constants/config values (was `src/constants`).
- `shared/config/` — env-driven runtime config (`index.ts` + `load-env.ts`).
- `shared/errors/` — `AppError` and subclasses.
- `shared/notifications/` — i18n message catalog + helpers.
- `shared/logger.ts`, `shared/csv.ts`, `shared/jwt.ts`.
- `shared/architecture/` — the import-boundary test.

Cross-cutting code depends on no layer and may be imported by any layer.

## Import direction

Allowed:

```text
Route → Controller → Service (BLL) → Repository/Model (DAL) → DB
```

`shared/*` may be imported by any layer. Disallowed:

```text
API (routes/controllers) → DAL          (skip the BLL)
BLL (services)           → Express / API
DAL (db)                 → BLL / API
shared/*                 → any layer
```

## Feature structure

Layer-first directories, one file per feature inside each layer (only create what
a feature needs):

```text
src/routes/<feature>.ts
src/api/controllers/<feature>.controller.ts
src/services/<feature>.ts            (+ mappers/validators when needed)
src/db/api/<feature>.ts              (repository)
src/db/models/<feature>.ts
src/shared/constants/<feature>.ts
```

## Module authoring (shared factories & helpers)

Most modules are assembled from shared factories/helpers — keep them that way.

- **Generic CRUD entity** = three one-line config files:
  - `src/services/<e>.ts` → `export default createCrudService(EntityDBApi, { notFoundCode });`
  - `src/api/controllers/<e>.controller.ts` → `export default createCrudController(service, { csvFields });`
  - `src/routes/<e>.ts` → `export default createCrudRouter(controller, { permission });`

  Factories: `services/shared/crud-service.ts`,
  `api/controllers/shared/crud-controller.ts`, `api/http/crud-router.ts` (generic
  over the repository's entity types — no casts). 18 of 21 entities use them;
  entities with genuinely different behavior (`users` invitations,
  `permissions` no-`globalAccess` queries) stay hand-written.
- **Repository (DAL)** = entity-specific `create`/`update`/`bulkImport`/`findBy`/
  `findAll`; the identical `remove`/`deleteByIds`/`findAllAutocomplete` delegate to
  `db/api/shared/repository.ts` (`removeRecord`, `deleteRecordsByIds`,
  `autocompleteByField`).
- **Feature service (BLL)** = reuse shared helpers: tenant/role access in
  `services/shared/access.ts` (`getOrganizationId`, `getOrganizationIdOrGlobal`,
  `hasGlobalAccess`, `requireUserId`, `hasRoleAccess(user, roleNames)`,
  `campusScope(user, tenantWideRoleNames)`, `assertAuthenticatedTenantUser`, …);
  validation in `services/shared/validate.ts` (`clampLimit`, `nullableString`,
  `requiredIsoDate`/`optionalIsoDate`); transactions via `db/with-transaction.ts`
  (`withTransaction(fn)`); CSV import via `services/shared/csv-import.ts`;
  `isRecord` from `shared/object.ts`.
  - `getOrganizationIdOrGlobal(user)`: returns `null` for global access users
    (bypassing org filter) or the user's org ID; throws `ForbiddenError` if neither.
  - `hasGlobalAccess(user)`: returns `true` when `app_role.globalAccess === true`.
  - `assertAuthenticatedTenantUser(user)`: allows global access users even without
    an organization (useful for platform-level admins).

## Error handling

Centralized — see `backend/docs/error-handling.md`. Handlers/services throw an
`AppError` subclass; the terminal `error-handler` middleware turns it into the
`{ message, code?, details? }` JSON body the frontend `ApiError` consumes.

### Global error handlers

The server registers process-level handlers in `src/index.ts` to prevent crashes
from unhandled errors:

- `process.on('uncaughtException')` — catches synchronous errors
- `process.on('unhandledRejection')` — catches unhandled promise rejections

These log the error and allow the server to continue running. This protects
against crashes from misconfigured external services (e.g., SMTP without
credentials) or unexpected async failures.

### Production credential guards

Development defaults (DB credentials, SECRET_KEY) are hardcoded in
`shared/constants/app.ts` for local development convenience. However, these
defaults are **never applied in production-like environments**:

- `shared/config/index.ts`: `requiredEnvWithDevDefault()` throws if `SECRET_KEY`
  is missing when `NODE_ENV` is `production` or `dev_stage`.
- `db/models/index.ts`: `validateProductionDbConfig()` throws if any `DB_*`
  credential is missing in production-like environments.

This ensures the server fails fast with a clear error message rather than
silently using insecure defaults.

## Enforcement & verification

- `src/shared/architecture/import-boundaries.test.ts` enforces the import
  direction. Hard invariants assert zero violations; the two remaining HTTP-in-BLL
  edge cases and the one DAL→BLL leak are capped by ceilings that must not grow.
- ESLint `no-restricted-imports` blocks (in `eslint.config.ts`) forbid the
  already-clean invariants at lint time (API→DAL, model/DAL/shared purity).
- `npm run typecheck`, `npm run lint`, `npm test` are the verification gates;
  `npm test` runs the Node test runner via `tsx` (error-handler + boundary tests).

## Known remaining items

- `services/file.ts` and `services/auth.ts` still depend on `req`/`res` (file
  streaming; session IP/UA + cookies). To be revisited with the upload subsystem.
- `db/api/file.ts` → `services/file` (GCloud delete) is a DAL→BLL leak to invert
  (the BLL should orchestrate blob + row deletion).
- `src/index.ts` remains the composition root + entry; an `app/server.ts` split is
  optional and deferred (deploy runs `dist/index.js`).
- Repositories still hand-roll the `findAll` filter→`where` building per entity; a
  declarative where-builder could dedup it, deferred until the data platform
  stabilizes (higher-risk Sequelize typing).
