Architecture
How TALA is put together — two repositories, 14 backend modules, an exclusive user-or-organization ownership model, and a global request pipeline.
TALA has one architectural idea and everything else follows from it: a row is owned by a person or by an organization — exactly one, never both, never neither.
Assets carry owner_user_id alongside organization_id; collections carry the same pair as user_id and organization_id. Each column is individually nullable, but a database CHECK constraint on both tables requires precisely one of the pair to be set. That is what makes ?scope=personal|organization work on the listing endpoints, and it is why every tenant-facing request resolves an org context before it touches the database.
CHK_assets_exactly_one_owner num_nonnulls("owner_user_id", "organization_id") = 1
CHK_collections_exactly_one_owner num_nonnulls("user_id", "organization_id") = 1
Modules
What each backend module owns and where it mounts.
Data model
19 entities, 3 join tables, and the ownership chain.
Request pipeline
The guards, pipes, interceptors, and filter every request passes.
The shape of a request
Every authenticated call to tenant data walks the same path.
ThrottlerGuard
60 requests per 60 seconds per IP, applied globally as APP_GUARD.
AuthGuard
Decrypts the AES-256-GCM envelope, verifies the JWT with algorithms: ['HS256'] pinned, rejects any account that is not VERIFIED, and scrubs password off req.user.
OrganizationGuard
Resolves the caller’s active membership and writes { organizationId, role } onto req.orgContext.
ValidationPipe
Whitelists the body against its DTO — unknown keys are dropped silently — and coerces param and query types.
Controller and service
The service re-checks ownership against the resolved context. A mismatch returns 404, not 403.
ActivityLogsInterceptor
Records the call after the response via tap(), without awaiting.
Two audit trails, deliberately
They answer different questions and neither replaces the other.
asset_logs |
activity_logs |
|
|---|---|---|
| Written by | services, inside the transaction | ActivityLogsInterceptor, after the response |
| Awaited | yes | no |
| Has HTTP context | no | endpoint, status, IP, user agent, duration |
| Survives a rollback | no — it rolls back with the write | yes — it is fire-and-forget |
| Answers | “what happened to this asset?” | “who called what, and did it work?” |
Identifiers
IDs are CSPRNG-generated, not sequential. generateId(prefix, suffixLength = 12) in src/lib/utils/id.util.ts produces values like ASa7Bk9x2Q….
The old scheme was PREFIX + (1000 + COUNT + 1), which both raced under concurrency and let anyone enumerate the table. It is gone — but legacy sequential IDs still exist in old rows.
