Architecture

On this page

dembrane is event-driven and split into several long-running processes that talk to each other through Postgres, Redis/Valkey and S3. This page is the map: what runs where, how the API is layered, and how a request is authenticated. For the build-and-run mechanics see local development; for how the model fans out into background work see the processing pipeline.

The services and their ports

Service Default port Code What it does
FastAPI backend :8000 echo/server/dembrane/ The HTTP API (v1 + v2), the BFF, and the service layer.
Agent service :8001 echo/agent/ Agentic chat (CopilotKit + LangGraph). Leased turns in Redis.
Directus :8055 echo/directus/ Data layer, authentication, file storage. 49 collections.
Dramatiq network workers - echo/server/dembrane/tasks.py gevent workers for async I/O: transcribe, merge, summarise, reports, emails.
Dramatiq cpu workers - tasks.py CPU-bound work (e.g. chunk merge). Single-threaded.
APScheduler - echo/server/dembrane/scheduler.py Blocking scheduler; fans periodic work out to Dramatiq.
Admin dashboard (dev) :5173 echo/frontend/ Vite dev server for the host dashboard.
Participant portal (dev) :5174 echo/frontend/ Vite dev server for the portal (same codebase).

Backing stores:

The mprocs.yaml at the repo root launches the host processes (server, workers, workers-cpu, scheduler, admin-dashboard, participant-portal); the devcontainer’s compose file runs the infra (Postgres, Redis, Directus). See local development.

One frontend, two surfaces

echo/frontend/ is a single React/TypeScript SPA that serves both the host dashboard (dashboard.dembrane.com) and the participant portal (portal.dembrane.com). It chooses which router to mount by hostname. The dashboard is the authenticated host experience; the portal is the unauthenticated participant experience (no account needed). In dev they’re two Vite servers (:5173 and :5174) so you can work on either in isolation.

The API: v1 and v2

The backend exposes two generations of API under echo/server/dembrane/api/:

New work generally lands in v2. v1 endpoints are kept where clients (the portal, webhooks, the iOS app’s upload path) still depend on them.

The BFF layer - api/v2/bff/

The backend-for-frontend layer (api/v2/bff/) exists to give the dashboard and the iOS app exactly the shapes they need, composed server-side, rather than making them stitch together several primitive calls. It currently covers:

Note

dembrane Go (iOS) calls the same v2/bff/* endpoints as the web dashboard, plus the v1 participant/* upload API. Keep BFF responses stable - two clients depend on them. See dembrane Go (the mobile app).

The service layer - service/

Business logic that’s shared across routers lives in echo/server/dembrane/service/ (agentic.py, chat.py, conversation.py, file.py, project.py, webhook.py). Routers stay thin; the service layer holds the rules so v1, v2 and BFF endpoints behave consistently. Above that sit module-level helpers - policies.py, seat_capacity.py, inheritance.py, billing_account.py, coordination.py, summary_utils.py, and so on.

Authentication

Auth is delegated to Directus. The backend validates the Directus JWT on each request via echo/server/dembrane/api/dependency_auth.py:

Authorisation beyond “is this a valid user / is this staff” is policy-based, not role-based: enforcement code calls has_policy(...), never checks the role string directly. That whole system - org/workspace role presets, tier gates, seats, inheritance - is covered in roles & policies in code. For the conceptual model see roles & permissions.

Important

The cookie name is configurable (settings.directus.session_cookie_name). Don’t hard-code directus_session_token; read it from settings.

How a request flows

A typical authenticated dashboard request:

  1. The SPA calls a v2/bff/* endpoint with the session cookie.
  2. dependency_auth validates the JWT and resolves the caller (and whether they’re staff).
  3. The router calls the service layer, which checks has_policy(...) for the action and reads/writes through Directus (or, for hot reads, pgvector/SQL).
  4. Anything slow or fan-out-shaped (transcription, summaries, reports, emails) is not done inline - the router enqueues a Dramatiq actor and returns. Progress streams back to the client over SSE, backed by Redis pub/sub. See the processing pipeline and background jobs.

The standalone agent service

Agentic chat does not run inside the FastAPI process. It’s a separate service in echo/agent/ (port :8001) built on CopilotKit + LangGraph, with its own settings and an echo_client.py that calls back into the backend for data. It coordinates turns with leases in Redis so a run isn’t processed twice. Standard (non-agentic) chat is served by the main backend. The split, the tools, and the lease runtime are covered in chat & the agent service.

Production note

The production API runs under a custom asyncio uvicorn worker (dembrane.asyncio_uvicorn_worker.AsyncioUvicornWorker) - we avoid uvloop for nest_asyncio compatibility. Locally, mprocs runs uvicorn with --loop asyncio --reload.


Related

Related

Comments