Skip to content

System Architecture ​

This page gives you a mental map of the backend: what it talks to, what it owns, and where the rules are enforced.

After reading this page, you should know how the backend is structured, and where the rules from Core Concepts are enforced.

System context ​

  • Backend is authoritative for money-changing operations (transactions, transfers, invoices, payouts).
  • Frontend is not trusted for correctness; it only proposes actions.
  • External systems are sources of truth for identity/structure and payments; the backend stores what it needs locally.

Request lifecycle (where things happen) ​

Most endpoints follow the same shape:

  1. RequestContextMiddleware assigns a request id, which every log line of the request carries.
  2. TokenMiddleware authenticates the request and attaches a token. It rejects tokens of deleted users.
  3. RequestValidatorMiddleware validates the request body structure against the Swagger spec.
  4. PolicyMiddleware checks RBAC and RestrictionMiddleware applies row-level restrictions.
  5. AsyncValidatorMiddleware runs registered business-rule specs (which may include async/DB checks). Runs after authorization so unauthorized requests never trigger DB-hitting validation.
  6. Controller handler translates HTTP to typed input/output.
  7. Service orchestrates domain logic across multiple entities.
  8. Entities persist data via TypeORM.

Both RequestValidatorMiddleware and AsyncValidatorMiddleware return { valid: false, errors: string[] } on failure so callers see a consistent 400 shape.

The practical rule: controllers should stay thin. Structural validation belongs in the middleware layers; deeper domain rules belong in a service or, when they need to run before the handler, in a spec registered with AsyncValidatorRegistry.

Logging ​

Log calls pass a constant message first and their variable parts in an object after it, for example logger.trace('invoice.delete', { id }). A constant message can be grouped and counted across requests; a message with values interpolated into it cannot. Never log a whole entity: log the id. Never log the caller either: the actorId is added for you.

Messages are event names of the form <domain>.<action>, such as user.update_pin or cron.sync_balances.started. A failure uses the event of the operation with .failed appended and passes the error as its own argument, for example logger.error('invoice.delete.failed', { id }, error). Wrapping it as { error } loses the message and stack.

LOG_FORMAT picks the output format. json emits one object per line and is the default in production; pretty emits readable lines and is the default everywhere else. Both formats annotate every line with the requestId of the request that produced it, and with the actorId once the token middleware has accepted a token. Code that runs outside a request, such as a cron task, has no such context, so anything it needs to record it must pass explicitly.

Where correctness is enforced ​

Money is not “best effort”. SudoSOS relies on a few hard guarantees:

  • Database transactions: money-changing operations are executed atomically. If part of the operation fails, none of it is committed.
  • Historical truth via revisions: products, containers, and points of sale are revisioned. Past purchases must keep referencing the revisions that were current at the time.
  • Balance cache: balances are stored in the balance table and updated from transactions and transfers. Treat it as a cache; the event tables are the real history.
  • Soft deletion: most entities are not hard-deleted, because financial history must remain auditable.

Authorisation (RBAC) ​

RBAC checks live in controller policies. A request is authorised based on:

  • action (get, create, update, delete)
  • relation (all, organ, own)
  • resource (e.g. Transaction, Invoice)
  • attributes (which fields may be accessed)

The important part for contributors: relation is computed per request (often by loading a resource and comparing its ownership).

What lives where (backend structure) ​

At a high level:

  • src/controller/: routes, RBAC policy, request/response DTOs.
  • src/service/: domain logic and orchestration.
  • src/entity/: database models (including revisioned entities).
  • src/middleware/: auth/token and request shaping.
  • src/rbac/: roles, permissions, and enforcement helpers.
  • src/gewis/: GEWIS-specific integrations (sync).

What this page intentionally does not cover ​

  • Endpoint-by-endpoint API reference. Use Swagger.
  • Entity-by-entity code reference. Use TypeDoc.

References

  • Swagger: https://sudosos.gewis.nl/api/api-docs/
  • TypeDoc: /typedoc/