# Architecture > Audience: operators running IRDB, and engineers building replacement > frontends. Read this first if you're new to the codebase. ## System overview IRDB is an **IP reputation database** — it ingests abuse reports about specific IP addresses, applies a decaying weighted score per category, and distributes tailored block lists to firewalls and proxies. Reporters (web servers, IDS, fail2ban-style agents) push events; each **consumer** (firewall, proxy) pulls a block list shaped by a named **policy** that decides which categories and thresholds count. The system is shipped as a small Docker Compose stack. Reporters and consumers talk JSON to the `api` over an authenticated REST surface; human operators use a thin PHP UI that calls the same api with a service token plus an impersonation header. The split is deliberate: the UI is replaceable, the api is the contract. ## Container topology ```mermaid flowchart LR subgraph clients[Clients] rep[Reporters
web/IDS/fail2ban] con[Consumers
firewalls/proxies] adm[Admins
browser] end subgraph stack[Compose stack] ui[ui
PHP+Twig BFF
:8080] api[api
Slim+FrankenPHP
:8081] migrate[migrate
one-shot Phinx] sched[scheduler
busybox crond] mysql[(mysql
optional)] end db[(SQLite or MySQL)] rep -- POST /api/v1/report ----> api con -- GET /api/v1/blocklist --> api adm -- HTTPS --> ui ui -- service token + X-Acting-User-Id --> api sched -- POST /internal/jobs/tick --> api migrate --> db api --> db mysql -.-> db ``` Five services in compose; only two run continuously (`api` + `ui`). `migrate` is one-shot (`restart: "no"`); `scheduler` is opt-in via the `compose.scheduler.yml` overlay; `mysql` is opt-in via uncommented config. Single-host SQLite deployments run with just `migrate` + `api` + `ui`; the data volume is shared between `migrate` and `api`. ## Where state lives | Where | What | |----------------------------------|------------------------------------------------------------| | `irdb-data` Docker volume | SQLite database file `/data/irdb.sqlite`, GeoIP MMDBs at `/data/geoip/`. Owned exclusively by the `api` container; `migrate` mounts it briefly to apply schema changes. | | `mysql-data` Docker volume | Optional MySQL `/var/lib/mysql`. Replaces the SQLite path when `DB_DRIVER=mysql`. | | `ui` container's writable layer | PHP file-backed sessions (`/tmp` per the FrankenPHP base). Tied to a single replica; sticky sessions required if scaling UI horizontally. | | `.env` | All secrets (service token, internal job token, OIDC client secret, MySQL password, local-admin Argon2id hash). | | **Browser localStorage** | Light/dark theme preference only. Session is in a Secure cookie. | Notably absent: the `ui` container has zero persistent state. Replace its image and nothing of operational importance is lost. The api owns everything. ## Stable surfaces vs replaceable parts The contract for future frontends, integrators, and replacement UIs. **Anything in the "stable" column should not break across `v1` minor releases.** | Surface | Stability | Notes | |------------------------------------------------------|----------------|----------------------------------------------------------------------------------------| | `/api/v1/*` paths and request/response shapes | **stable** | Additive changes only within `v1`. Breaking changes ship as `v2`. OpenAPI is canonical. | | Token kinds (`reporter`, `consumer`, `admin`, `service`) | **stable** | Strings are persisted in `api_tokens.kind` and used in audit logs. | | RBAC roles (`viewer`, `operator`, `admin`) | **stable** | Persisted in `users.role` and `api_tokens.role`. | | `users` shape and `oidc_role_mappings` semantics | **stable** | Future SPAs/native UIs upsert via `/api/v1/auth/users/upsert-oidc` the same way the BFF does today. | | Service-token + `X-Acting-User-Id` impersonation | **stable** | The pattern any BFF replacement uses; the API doesn't trust the header without a service token. | | Error envelope `{error, details?}` | **stable** | Validation errors include `details`; auth failures don't. | | Audit `actor_kind` enum | **stable** | `user`, `admin-token`, `reporter`, `consumer`, `system`. Used by external SIEM exporters in future work. | | Twig template names under `ui/resources/views/` | replaceable | Tied to the current UI implementation; replacement UIs render however they like. | | UI route paths under `/app/*` | replaceable | Browser-only; not part of any contract. | | Internal class names, method signatures, file layout | replaceable | Refactoring is fair game. | | `/internal/jobs/*` | replaceable | Scheduler-only; no public guarantee. Not in OpenAPI. | | Twig globals, htmx attributes, Alpine components | replaceable | Implementation details of this UI. | ## Why this split **Backend-for-Frontend (BFF) pattern.** The `api` is a pure JSON service: no HTML, no sessions, no cookies. The `ui` is a thin browser-facing process that owns OIDC handshakes, browser sessions, CSRF, and Twig rendering — but no business logic, no scoring, no database. Every admin page on the UI fetches from the api with the service token plus the acting user's id. Why this matters: 1. **The UI is replaceable.** A team that wants Vue, Svelte, or a Tauri desktop app can replace the `ui` container without touching the api. The contract is the OpenAPI document and the impersonation pattern. See [`frontend-development.md`](./frontend-development.md). 2. **No HTML in the api.** Other clients (firewalls, fail2ban, monitoring) call the same admin endpoints the UI does. If the api started rendering Twig, those integrations would suddenly carry HTML they don't want. 3. **Auditing is honest.** The api records the impersonated user as the actor (`actor_kind=user`) when called via the BFF, never the service token. An admin token used directly records as `actor_kind=admin-token`. Future external authentication paths can plug in alongside without breaking the audit trail. 4. **The api can scale independently.** With MySQL, the api is stateless and can run as N replicas behind a load balancer; the `job_locks` table mediates between them. The UI is typically single-replica because of file-backed sessions. The trade-off: every UI request makes one or more outbound calls to the api, which costs latency and an HTTP hop. In practice the containers run on the same Docker network and the per-request cost is sub-millisecond. ## Where the rest of the docs live - [`api-overview.md`](./api-overview.md) — the public API surface, with worked examples. - [`auth-flows.md`](./auth-flows.md) — every authentication flow (machine, BFF, OIDC, local admin), Entra setup, future user-token sketch. - [`frontend-development.md`](./frontend-development.md) — the headline doc for replacement UIs. - [`api-reference.md`](./api-reference.md) — short. Pointer to OpenAPI plus the things OpenAPI doesn't cleanly express (rate limits, ETag semantics, impersonation header, response conventions). The OpenAPI document itself is at `api/public/openapi.yaml` and served at `/api/v1/openapi.yaml` with a viewer at `/api/docs`.