---
title: Modules
description: The 14 backend feature modules — where each one mounts, what it owns, and the route-ordering and scoping rules that are easy to get wrong.
---

The backend is 167 TypeScript files across 14 feature modules plus the app root. The global prefix is `api`, with `/health` explicitly excluded.

```ts title="src/main.ts"
app.setGlobalPrefix("api", {
  exclude: [{ path: "health", method: RequestMethod.GET }],
});
```

## The map

| Module or controller | Mount | Routes | Owns |
| --- | --- | --- | --- |
| `AuthModule` | `/api/auth` | 14 | Registration, verification, password reset, login, OAuth, refresh rotation, logout |
| `ProfileController` | `/api/auth/user` | 3 | Display name, avatar presign and commit — lives inside `AuthModule` |
| `CollectionModule` | `/api/collections` | 6 | CRUD, pagination, per-user favourites |
| `AssetsModule` | `/api/assets` | 12 | Read, update, delete, publish, download, versions, restore, asset logs |
| `UploadModule` | `/api/upload` | 3 | S3 multipart orchestration |
| `OrganizationModule` | `/api/organizations` | 10 | Organizations, members, invitations, and both organization guards |
| `ProjectModule` | `/api/projects` | 6 | Project CRUD and board ordering |
| `TaskModule` | `/api/tasks` + `/api/projects/:projectId/tasks` | 8 | Tasks, board moves, assignees |
| `TaskCommentModule` | `/api/tasks/:taskId/comments` | 4 | Threaded comments with asset attachments |
| `DashboardModule` | `/api/dashboard` | 1 | Org-scoped counts and recent task activity |
| `ActivityLogsModule` | `/api/timeline` | 4 | HTTP audit log, the global interceptor, a retention cron |
| `PlanModule` | `/api/plan` | 1 | Read-only subscription plans |
| `SeederModule` | `/api/seeder` | 1 | Plan seeding, ADMIN only |
| `AssetVersionModule` | — | 0 | Entity and repository only; surfaced through `AssetsModule` |
| `SharedModule` | — | 0 | S3, CloudFront, paginator, token generator, query runner — `@Global` |
| `AppController` | `/health` | 1 | Liveness, outside the `api` prefix |

## Two mounts that are not where you would look

**`TaskController` is root-mounted.** Its `@Controller()` decorator takes no prefix, so it declares both `/api/tasks/*` and `/api/projects/:projectId/tasks` from the same class. If you go looking for project tasks under `src/project/`, you will not find them there.

**`ProfileController` is not its own module.** It lives in `src/auth/profile.controller.ts` and mounts at `/api/auth/user`, so its routes sit a segment deeper than `AuthController`'s: `/api/auth/user/profile`, `/api/auth/user/avatar-upload`. Nothing collides with `GET /api/auth/user` — the paths are simply different.

## Route declaration order matters

`AssetController` mounts `GET logs` above `GET :id` with a comment explaining why:

```ts title="src/assets/asset.controller.ts"
// NOTE: this MUST stay above @Get(':id'). Nest matches routes in declaration
// order, so a later literal path loses to an earlier parameterized one.
@Get("logs")
async getMyLogs(/* … */) {}

@Get(":id")
async getAssetById(/* … */) {}
```

Reorder those two and `/api/assets/logs` starts resolving as an asset whose ID is the literal string `logs`.

## Transactional writes

Multi-step database writes go through `QueryRunnerExec` in `src/shared/services/query-runner-exec.service.ts`. Its `commit` and `rollback` always release the runner and are idempotent, so a double-rollback in an error path is safe.

Upload completion is the canonical example: asset, version, and log rows are persisted in a single transaction, so a failure never leaves a version row pointing at an asset that does not exist.

## Custom exceptions

Throw `ApplicationException` from `src/lib/exception/app.exception.ts` for expected 4xx conditions. It is a bare `Error` subclass — services catch it and translate to the right Nest exception:

```ts
if (error instanceof ApplicationException)
  throw new NotFoundException(error.message);

this._logger.error((error as Error).message);
throw new InternalServerErrorException("Something went wrong");
```

Everything else reaches [the global filter](/architecture/request-pipeline#exception-filter).

## Scoping is not enforced by the compiler

Asset and collection reads and writes verify `Asset → Collection → User == req.user` and return **404** on mismatch, so a probe cannot distinguish "not yours" from "does not exist".

:::danger[When you add an endpoint that touches tenant data, scope it explicitly]
A missing ownership check is a silent IDOR, not a compile error. Nothing in the type system will tell you it is absent.
:::
