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

# Reporter architecture

> How Reporter runs: one binary whose RUN_MODE selects the API surface, the worker surface, or both, the queue between them, and the stores they share.

Reporter ships as **one binary**. `RUN_MODE` selects which surfaces that binary serves: `api`, `worker`, or `all`. The API and the worker are two roles of the same program, not two products, and they are built, versioned, and released together.

That single fact shapes everything else on this page. You choose a topology at deploy time by setting an environment variable, not by assembling separate services.

## Two surfaces, one binary

***

| Surface | `RUN_MODE` | Serves                                                                                                              |
| ------- | ---------- | ------------------------------------------------------------------------------------------------------------------- |
| API     | `api`      | Every REST operation on port `4005`, plus `/health`, `/readyz`, and `/version`.                                     |
| Worker  | `worker`   | No REST surface. Consumes the report queue. A small health server on `HEALTH_PORT` carries `/health` and `/readyz`. |
| Both    | `all`      | Both surfaces in one process.                                                                                       |

```bash theme={null}
# API surface
RUN_MODE=api
SERVER_ADDRESS=:4005

# Worker surface
RUN_MODE=worker
HEALTH_PORT=4006
```

Run `all` when one process is enough, which is the usual choice for local development and small deployments. Split the roles into two deployables when report generation needs to scale on its own: rendering a large PDF costs far more than accepting the request that asked for it, and separate deployments let you size each side to its own load.

## The path a report takes

***

<Steps>
  <Step title="The API accepts the request">
    A `POST` to `/v1/reports` names a template and its filters. Reporter takes an idempotency lock first, keyed on the `X-Idempotency` header when you send one and on a hash of the request body when you do not. A duplicate that is still in flight is rejected; a duplicate of a completed request replays the original report and marks the response as a replay.
  </Step>

  <Step title="The API validates and persists">
    Reporter loads the template's field map and output format, checks the filter fields against the live schema of each data source, and writes the report with status `Processing`.
  </Step>

  <Step title="The API queues the work">
    It publishes a command message to the internal RabbitMQ queue and answers `201 Created` with the `Processing` report. The caller is done here. Rendering has not started.
  </Step>

  <Step title="The worker renders">
    The worker consumes the command, skips it if the report already settled as `Finished` or `Error`, loads the template from object storage, extracts the data for each data source, renders the document, converts it to PDF when the format calls for it, and uploads the artifact.
  </Step>

  <Step title="The worker settles the state">
    It writes `Finished`, `Partial`, or `Error` and emits the matching event. The download operation serves the artifact once the report is `Finished`.
  </Step>
</Steps>

## The queue between them

***

The API and the worker communicate over one RabbitMQ queue that carries report commands in one direction, with a dead-letter queue behind it. This queue is private plumbing between the two surfaces of the same binary. It is not an integration point, it carries no contract you should build against, and its exchange, queue, and routing key are all operator-configured.

Business events are a separate channel. Reporter publishes them to their own exchange, which the operator configures and which the deployment's reference value names `reporter.events`. Every emission happens after the database commit and never fails the work that produced it. High-value events go through a durable outbox rather than a direct publish, so a broker outage delays them instead of losing them.

## Data extraction

***

The worker does not talk to your databases through hand-written queries. It runs the same extraction engine that powers [Fetcher](/en/fetcher/fetcher-core-concepts), embedded in process, with no network hop to a separate service.

The engine bounds every run. The defaults allow 10 data sources per report, 50 tables per data source, 200 fields per table, four data sources extracted in parallel, and a five-minute deadline for the whole extraction. Failures are collected rather than fatal: a report whose sections partly succeed renders from what it has and settles as `Partial`, recording which sections failed.

## Stores and artifacts

***

| Dependency                   | Role                                                               |
| ---------------------------- | ------------------------------------------------------------------ |
| MongoDB                      | Templates, reports, deadlines, and the event outbox.               |
| RabbitMQ                     | The internal report queue and the business-event exchange.         |
| Redis or Valkey              | Idempotency locks, the schema cache, and tenant lifecycle signals. |
| S3-compatible object storage | Template files and rendered artifacts, in one bucket.              |

Object storage is S3-compatible: Reporter speaks the S3 protocol and any service that answers it works as a target. Template sources land under the `templates/` prefix, and rendered artifacts under `reports/`, keyed by template and report identifier with the output format as the extension. Reporter applies no expiry of its own, so retention is a lifecycle policy you set on the bucket.

## Reporter and Midaz

***

Midaz is a **data source** to Reporter, not a runtime dependency. Reporter carries no Midaz-specific code: a Midaz database is registered with the same `DATASOURCE_*` variables as any other PostgreSQL database, and templates address it by the `configName` the operator chose.

Two consequences follow. Reporter starts and serves templates, deadlines, and metrics with no data source configured at all, so a Midaz outage never stops Reporter from running. And the same deployment can report over Midaz, over your own databases, and over both in one template.

Point Reporter at a read replica where one exists. The extraction path only reads, but the query load of a heavy report is real, and a replica keeps it off the ledger's write path.

<Note>
  See [Connecting Reporter to Midaz](/en/reporter/connecting-reporter-to-midaz) for the variables, the `configName` convention, and how to confirm the source is visible.
</Note>

## Health and readiness

***

Both surfaces expose `/health` and `/readyz` before authentication, so probes reach them without a token. `/health` answers 503 until the startup self-probe succeeds, which lets an orchestrator restart a pod that could not reach its dependencies at boot. `/readyz` reports the dependencies that surface actually uses, and its response states which deployment mode the process is running in. The API surface also serves `/version` with the build provenance.

Read the [health and readiness reference](/en/reference/health-and-readiness) for the probe contract shared across Lerian products.

## Next steps

***

<CardGroup cols={2}>
  <Card title="Core concepts" icon="cubes" href="/en/reporter/reporter-core-concepts">
    Templates, data sources, reports, deadlines, and the four report states.
  </Card>

  <Card title="Environment variables" icon="gear" href="/en/reporter/reporter-environment-variables">
    Every setting that shapes a Reporter deployment.
  </Card>
</CardGroup>
