# Backend shape

Why every backend feature has the same files, why there is no barrel and no repository layer, and where Effect runs.

The server code in `packages/api` follows one idea: **every feature has the same shape, and dependencies flow one way**. Once you have read one feature, you can read all of them.

## Features, not layers [#features-not-layers]

A feature is a folder, `packages/api/src/features/<feature>/`. Inside it, each file's name states its role: the contract clients import, the procedures oRPC exposes, the service that does the work, the errors it can fail with, the jobs the cron tick runs and the raw HTTP edges. The [backend conventions](/docs/development/backend-conventions) list the roles and what each may import.

Grouping by feature instead of by layer keeps one capability in one place. You can follow a change without hopping between `routers/`, `services/` and `lib/`, and you can delete a feature as a folder. Shared building blocks that belong to no feature (procedure builders, the request context, the Effect edge, the job runner) live in `packages/api/src/platform/`.

## Boundaries the build can check [#boundaries-the-build-can-check]

Three boundaries matter, and each one is enforced by a check rather than by discipline:

- **Client and server.** A client imports only a leaf contract, an enum or a type. A barrel is how server code once reached the browser, so the package has none; its public surface is an enumerated `exports` map ([ADR-0002](/docs/adr/0002-no-barrels-on-server-capable-modules)).
- **Tenant and system.** Procedures read and write through `context.db`, which runs as `app_tenant`. Row-level security, not a layer of code, keeps Organizations apart ([ADR-0009](/docs/adr/0009-row-level-security)). That is why there is no repository layer: it would forward calls to the same Drizzle handle and add a second place for every query without adding isolation.
- **Feature and feature.** A feature uses another feature's contract, service or errors, not its internals. Only the app router imports procedures, and only the job runner imports jobs.

The [enforcement matrix](/docs/development/enforcement) shows what checks each boundary.

## Errors and Effect [#errors-and-effect]

A service describes what can go wrong as tagged errors. The procedure turns them into the stable codes clients localize, once, in one mapper. Services that a migration has reached are Effect programs: the procedure runs one program per call, so typed failures, deadlines and the rule that a started write is never abandoned have a single owner. Contracts stay Zod, routing stays oRPC and queries stay Drizzle ([ADR-0010](/docs/adr/0010-procedures-run-one-effect-program), [ADR-0005](/docs/adr/0005-effect-boundary)).

## Smells [#smells]

You have drifted from this shape when you see any of these:

- a procedure file full of queries in a migrated feature;
- a service that imports `@orpc/*`;
- a contract that imports a Drizzle table or a service;
- a feature importing another feature's queue or lease module;
- a list endpoint that pages differently from its neighbours;
- a new error that is a string instead of a tagged error.

Start a new feature with the `domain-feature` skill, which walks through these steps in order.

