> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> Three services, a worker that turns the crank, fifteen shared packages, and a data plane partitioned by mode. The whole system at altitude.

Read this once at altitude, then drill into any box.

Duro is a pnpm + Turborepo monorepo. Three deployable Node services, a constellation of `@duro/*` packages they share, and a Postgres/Redis/R2 data plane. No Docker in the hot path — services run under PM2 on a VPS.

## The whole system

```mermaid theme={null}
flowchart TB
    subgraph callers["Callers"]
        DASH["Dashboard<br/>(session cookie,<br/>10-min idle)"]
        MSRV["Merchant server<br/>(sk_ keys / OAuth)"]
        SDK["Inline SDK<br/>duro.pay()"]
        CUST["Customers<br/>(WhatsApp OTP)"]
        STAFF["Duro staff<br/>(KYC review)"]
    end

    subgraph edge["Edge"]
        CF["Cloudflare DNS + TLS"]
        NX["nginx :443"]
    end

    subgraph services["Services (PM2)"]
        CORE["core-api<br/>auth · onboarding · KYC ·<br/>members · API keys · OAuth ·<br/>appearance · email templates ·<br/>notifications · staff KYC"]
        PUB["public-api<br/>merchant REST (/v1) ·<br/>hosted checkout ·<br/>identity + OTP · portal"]
        WORK["billing-worker<br/>BullMQ consumers +<br/>repeatable scanners"]
    end

    subgraph pkgs["Shared packages (@duro/*)"]
        BILL["billing<br/>state machine · proration ·<br/>dunning strategy · payday · cycle"]
        DB["db<br/>two Prisma clients ·<br/>repositories · RepositoryContext"]
        HK["http-kit · auth · crypto ·<br/>validation · payments · queue ·<br/>email · whatsapp · cache · r2"]
    end

    subgraph data["State"]
        PGC[("core schema<br/>tenants · members · sessions ·<br/>KYC · store config · identities")]
        PGD[("sandbox + live schemas<br/>plans · customers · subs ·<br/>invoices · dunning · webhooks")]
        RDS[("Redis<br/>BullMQ · cache · OTP")]
        R2[("R2<br/>KYC docs")]
    end

    subgraph ext["External"]
        GW["Charge gateway"]
        WA["WhatsApp Cloud API"]
        SMTP["SMTP"]
        MHOOK["Merchant webhooks"]
    end

    callers --> edge --> services
    STAFF --> CORE
    services --> pkgs
    CORE --> PGC
    PUB --> PGC & PGD & RDS
    PUB --> WA
    WORK --> PGD & PGC & RDS
    WORK --> GW & SMTP & MHOOK
    CORE -. enqueue email .-> RDS
    RDS == jobs ==> WORK
```

## Why three services, not one

A clean separation of **trust boundaries** and **runtime shapes**:

| Service            | Talks to                             | Auth                                            | Why it's separate                                                                                                                                               |
| ------------------ | ------------------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **core-api**       | the dashboard + Duro staff           | session cookies, staff key                      | First-party surface. Holds the keys to identity, KYC, and tenant config. Never exposed to merchant servers.                                                     |
| **public-api**     | merchant servers, the SDK, customers | `sk_`/`pk_` keys, OAuth tokens, identity tokens | The internet-facing API. Multi-auth, rate-limited, idempotent. Its blast radius is one tenant's data.                                                           |
| **billing-worker** | nothing inbound (a health port)      | none                                            | A pure background process. No HTTP surface for business logic — it only consumes queues and runs scanners on a timer. Scaling it never touches request latency. |

The dashboard and the public API are different audiences with different threat models, so they're different processes. The worker has a completely different runtime shape — long-running jobs, no request/response — so it's isolated where a slow charge can never block a checkout.

## The packages do the real work

Controllers are thin. The intelligence lives in framework-agnostic packages so it's testable in isolation and reusable across services:

<CardGroup cols={2}>
  <Card title="@duro/billing" icon="calculator">
    Pure logic, zero IO. The subscription state machine, proration math, the dunning decision engine, payday windows, retry schedules, billing-cycle date math. 35 unit tests. This is the brain.
  </Card>

  <Card title="@duro/db" icon="database">
    Two generated Prisma clients (core + data), a `RepositoryContext` that hands a controller exactly the tenant-scoped repositories it needs, version-bump cache invalidation, cursor pagination.
  </Card>

  <Card title="@duro/http-kit" icon="plug">
    `BaseController`, responders, the `PermissionGuard`, tenant-scope and API-key middleware, idempotency, pagination parsing. Write a controller in twenty lines.
  </Card>

  <Card title="@duro/payments / queue / whatsapp / email" icon="cubes">
    Thin abstractions over the outside world — a `ChargeGateway` interface (simulated now, a live rail in July), BullMQ factories, the WhatsApp Cloud client, the SMTP mailer. Swap the implementation, keep the callers.
  </Card>
</CardGroup>

## A request's worth of moving parts

A merchant `POST /v1/subscriptions` touches a precise, short chain:

```mermaid theme={null}
flowchart LR
    R["Request<br/>sk_test_…"] --> AK["ApiKeyAuth<br/>verify key, set<br/>tenant + mode"]
    AK --> TS["TenantScope<br/>build RepositoryContext<br/>on req.repos"]
    TS --> ID["Idempotency<br/>(per-tenant key)"]
    ID --> CT["SubscriptionController"]
    CT --> SV["SubscriptionService<br/>(@duro/billing logic)"]
    SV --> REPO["repos.subscriptions<br/>data(mode) client"]
    REPO --> PG[("live or sandbox<br/>schema")]
    SV --> EV["events.record(...)"]
    EV -. later .-> WORK["webhook worker<br/>fans out signed delivery"]
```

The key move: by the time the controller runs, `req.repos` is already a `RepositoryContext` bound to the right tenant and the right schema. The controller cannot accidentally read another tenant's data — it has no client that can. [See multi-tenancy →](/architecture/multi-tenancy)

## What runs on a timer

The worker is the only thing in the system that acts without a caller. Four repeatable scanners, each a cross-tenant sweep that enqueues per-item jobs:

```mermaid theme={null}
flowchart TB
    subgraph scanners["Repeatable scans (BullMQ)"]
        BS["billing scan<br/>every 60s"]
        RS["reminder scan<br/>every 60m"]
        DS["dunning scan<br/>every 60s"]
        WS["webhook scan<br/>every 15s"]
    end
    BS -->|"subs due to renew"| RENEW["renew job"]
    RS -->|"subs renewing soon"| REMIND["remind job"]
    DS -->|"retries now due"| DUN["process job"]
    WS -->|"undispatched events<br/>+ due deliveries"| DELIVER["deliver job"]
    RENEW & REMIND & DUN & DELIVER --> EMAIL["email queue"]
```

Each scanner runs across **both** test and live schemas — a background process has no "current mode," so it must process everyone. Request-scoped code never does this; only the worker. [See queues & workers →](/platform/queues-and-workers)

## Conventions, enforced

These aren't style preferences — they're checked in CI and they shape how the code reads.

* **OOP everywhere.** Zero free `export function`s in business logic. Behaviour lives in classes with static or instance methods. Presenters, services, gateways, guards — all classes.
* **No `any`, ever.** `strict` TypeScript with `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`.
* **No comments.** The code is named to be read. The explanation lives here, in the docs, not in the source.
* **Exact version pins.** No `^`/`~`. Lockfile is law.
* **Every mutation is verified live.** The integration suite runs the real money loop against Postgres on every push.

Next: the [data model](/architecture/data-model) — how the three schemas are laid out and why.
