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

# Architecture

> Why the code is arranged the way it is, and what each boundary buys.

Why the code is arranged the way it is, and what each boundary buys.

## The shape

```
apps/web        React 19, Vite, Zustand, TanStack Query
apps/api        NestJS, TypeORM, PostgreSQL
packages/contracts   zod schemas + typed HTTP client, used by both
```

The contracts package is the load-bearing part. Both sides import it, so a change
to a request or response shape is a compile error on both sides at once, rather
than a disagreement discovered in production.

## Why hexagonal on the API

Each feature module is arranged as:

```
domain/          entities, invariants, value objects — no framework types
application/     use cases, depending only on ports
infrastructure/  TypeORM repositories, external adapters
interface/http/  controllers, pipes
```

Dependencies point inward. A use case declares what it needs as a port; the
infrastructure layer implements it and the module wires the two together in one
place. Three things follow:

* **The domain is testable without a database.** Segment matching and campaign
  status rules are pure functions over their own types.
* **The seed script and a future job reuse the use cases**, not a parallel
  implementation. `customers.import` is the same code whether a person or a
  scheduled task triggers it.
* **Framework types stop at the controller.** A `Repository` from TypeORM never
  reaches a use case, so swapping persistence does not reach into business rules.

### Where composition happens

`app.module.ts` is the only place that knows every module exists. A feature module
never imports another, which is what keeps the dependency graph acyclic and
readable.

## Why the contract is generated into OpenAPI

The alternative — `@nestjs/swagger` decorators on controllers — describes the same
facts a second time, in a place the compiler does not check against the client.
The two inevitably disagree. Generating the specification from the registry means
the published reference cannot describe an endpoint that does not exist, or omit a
field the API accepts.

The rendered UI is Swagger UI, served from the installed package rather than a
CDN: the reference has to work on an isolated network, and a third party must not
be able to change what a reader is shown. See
[the shared contract](/explanation/contracts).

## Why the version is in the path

`/api/v1/customers` rather than a header, a media type, or no version at all. See
[versioning](/explanation/api-versioning).

## Why responses are validated on arrival

The client validates every response against the contract before handing it to the
application. This costs a parse per response and buys something specific: a
deployment mismatch — a new API version, a proxy rewriting a body, a stale cached
bundle — fails loudly at the boundary instead of producing `undefined` three
layers into a component.

```ts theme={null}
if (!parsed.success) throw new Error(`Contract violation on "${name}": …`)
```

Loud and early is the intent. A silent cast here would move the failure to the one
place that cannot explain it.

## Why the model runs server-side

The browser cannot hold a provider key, and a prompt is business logic that
belongs under version control. The API owns a model port with four
implementations: the Anthropic adapter, the Gemini adapter, the OpenCode Zen
adapter, and a deterministic local generator. One decision made at boot binds
either port, so tests and development are offline and repeatable while production
gets a hosted model. Which adapter is bound comes from configuration alone —
adding a key is the only step needed to change models.

See [the generative pipeline](/explanation/generative-pipeline), and
[add an LLM provider](/how-to/add-an-llm-provider) for the steps a new provider
follows.

## Why state is split between Zustand and TanStack Query

They answer different questions.

* **Zustand** owns client state that no request produces: whether a session
  exists, which tokens are held. The HTTP client reads it outside React, because
  it needs a token on every call.
* **TanStack Query** owns anything that came from the server: customers,
  segments, campaigns, KPIs.

The practical consequence is that a write invalidates a cache rather than
notifying a component. Importing customers refreshes the table, the KPIs and the
dashboard because all three read the same keys — with no event bus to keep in
sync. See [client state](/explanation/state).

The one thing that is neither is a form draft: local component state, unsaved and
unshared.

## What is deliberately absent

* **A service layer between controller and use case.** It would only forward
  arguments.
* **Interfaces generated by hand for responses.** The registry entry is the type.
* **A mock database in the API test suite.** The port is faked at the test
  boundary instead, so tests stay fast without pretending persistence is
  optional.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.