# Architecture

The backend is a Bun and TypeScript monorepo in `dembrane/platform`: five apps built from packages in three layers, on PostgreSQL only. The frontend is one React and Vite codebase in `dembrane/frontend`.

This page is the map. The source of truth is `dembrane/platform/README.md` for the apps and the layer rule, and `dembrane/platform/PACKAGES.md` for every package.

## The stack

| Concern | Library |
|---|---|
| HTTP | Hono |
| Database | Drizzle ORM with postgres.js |
| Jobs and schedules | DBOS, on Postgres |
| Sign-in and sessions | Better Auth |
| Language models | The Vercel AI SDK with the Google Vertex provider |
| Agent access | The MCP SDK |

PostgreSQL is the only stateful service the platform itself needs. There is no Redis, no Directus service, no Python process and no separate agent service. Audio and files live in S3-compatible object storage.

The Bun version is pinned in `dembrane/platform/.bun-version`.

## The five apps

`apps/` holds the programs that deploy. Each has a `src/main.ts` that reads config, wires packages together and starts.

| App | What it does |
|---|---|
| `api` | The HTTP server. It mounts every namespace's routes and answers the dashboard, the portal and outside agents. |
| `worker` | Runs queued jobs and schedules: the audio pipeline, analysis, reports, the assistant, emails, billing timers. |
| `media` | The ffmpeg service. It probes, converts, splits and merges audio, one job per instance. |
| `web` | Serves the built dashboard and participant portal, and forwards `/api` to the API. |
| `migrate` | A one-shot job that runs before every rollout: schema migrations, the queue schema, database grants, the default verification topics. |

Each app builds to one compiled binary (`bun build --compile`) on a distroless base image. The media image adds a static ffmpeg and ffprobe. The web image also holds the built frontend.

An app loads only the config sections it reads. The worker never gets the auth secret. The media app has no database, no bucket credentials and no secrets: callers hand it presigned URLs to read from and write to.

## The three layers

A dependency points down only.

1. Apps may import anything.
2. Namespaces are product areas, such as `projects`, `conversations`, `chats`, `reports` and `billing`. Each owns its routes, jobs, storage and business rules. A namespace imports capabilities. It imports another namespace only when its `package.json` lists that namespace under `dembrane.allow` with a reason.
3. Capabilities are building blocks with no business rules, such as `config`, `db`, `auth`, `access`, `queue`, `storage` and `llm`. A capability never imports a namespace or an app.

`bun run packages check` enforces the rule. It is part of `bun run check` and of CI.

`PACKAGES.md` lists every package with one line each, and every allowed import between namespaces with its reason. In release 3.1.0 that is 24 namespaces and 20 capabilities. The file is generated by `bun run packages write` from each package's `package.json` and from the imports.

Two names are easy to confuse. `account` is the signed-in user's own account: profile, onboarding, invites, email. `accounts` is customer accounts as the dembrane team runs them.

Inside a namespace the pattern is the same everywhere: `routes.ts` returns a Hono router, its handlers call `service.ts`, and the service calls `storage.ts`. A small namespace such as `packages/webhooks/src` is a good first read.

## One frontend, two surfaces

`dembrane/frontend` builds once and serves two surfaces: the dashboard for hosts and the portal for participants.

- The `web` app decides which surface an instance serves with `WEB_ROLE` (`dashboard` or `portal`).
- It serves `/runtime-config.js`, which gives the frontend its environment, role and URLs before any module loads. So one build runs in every environment.
- When `WEB_API_ORIGIN` is set, the web app forwards `/api` to the API. The browser stays on one origin and sign-in cookies stay first-party.
- Unknown paths get `index.html`, so client-side routes load on refresh.
- In local development the two Vite servers stand in for the web app: the dashboard on port 5173 and the portal on port 5174.

## How a request flows

The order is set in `apps/api/src/app.ts`.

1. Correlation middleware gives the request an id and a logger.
2. The caller's address is resolved once, with the environment's trusted proxies.
3. Secure headers are added.
4. CORS applies to `/api/*`. The allowed origins are the dashboard URL and the portal URL only, with credentials.
5. `/api/auth/*` goes to the Better Auth handler.
6. The session middleware reads the session (cookie or bearer token) once and sets the principal: `appUserId`, `directusUserId`, `isStaff`. It never rejects. A route that needs a user calls `requireUser`, which answers 401 with the code `auth.session_expired`.
7. The namespace's router handles the route. The service checks the caller with `projectFor` or `workspaceFor` from `@dembrane/http`, which take a policy name. See [roles and policies in code](./roles-and-policies.md).
8. Every error becomes one JSON body: `{ detail, code, params, action }`.

Route paths were kept from the previous API: `/api/*`, `/api/v2/*` and `/api/v2/bff/*`. The `legacy-shape` package keeps the response shapes and the number and time formats the old API produced.

Identity says who someone is. What they may do is decided in `packages/access`. Sign-in itself is covered in [authentication](../developer-external/authentication.md). A participant has no account and no session. The [participant API](../developer-external/participant-api.md) page covers how portal calls are authorised.

## How a background job flows

1. A namespace defines the job with `defineJob(name, schema, options)`.
2. A service enqueues it through a `JobSink`. It can pass its database transaction, so the job exists only if the transaction commits.
3. The API holds an enqueue-only client. It never executes jobs.
4. The worker registers a handler and runs the job. A plain job is one retried step. A workflow is a durable multi-step flow whose step results are checkpointed in Postgres.
5. After a crash a workflow resumes at its first unfinished step, on any worker.

[Background jobs and schedules](./background-jobs-and-scheduler.md) has the detail. [The processing pipeline](./processing-pipeline.md) is the largest user of workflows.

## Live updates

- Live updates use Postgres `LISTEN` and `NOTIFY`, fanned out to server-sent event streams.
- Each API instance holds one `LISTEN` connection (the `Hub` in `packages/realtime`) that feeds every open stream.
- A publisher calls `publish`, which runs `pg_notify`. Inside a transaction the event is delivered on commit.
- Events are small nudges (ids and a type), not data. Delivery is best effort: a page catches up when its stream reconnects.
- Presence (participant pings, the live monitor) and rate-limit counters are rows in Postgres tables.

Every process connects to Postgres directly. A transaction-mode connection pooler breaks the job queue and the live streams.

## Health endpoints

| Endpoint | App | Answers |
|---|---|---|
| `/health`, `/api/health` | `api` | Liveness, with the release. |
| `/ready` | `api` | Whether the database answers. |
| `/ready/worker` | `api` | Whether a worker heartbeat is recent. |
| `/config.json` | `api` | The config values marked public. |
| `/health` | `web`, `media` | Liveness. |

## Related

- [Internal developer overview](./index.md)
- [The data model](./data-model.md)
- [The processing pipeline](./processing-pipeline.md)
- [Background jobs and schedules](./background-jobs-and-scheduler.md)
- [Roles and policies in code](./roles-and-policies.md)
- [Local development](./local-development.md)
- [Configuration and language model providers](../developer-external/configuration-and-llm-providers.md)