---
title: Architecture
description: C4-style overview of Enterium — a modular monolith with CI-enforced bounded contexts, a React web UI, immutable native pipelines and vendored publishing services.
locale: en
version: 0.2.0
canonical: https://enterium-developer-docs.pages.dev/developers/platform/architecture/
---

> Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json.

Enterium is a **modular monolith**: one FastAPI backend split into bounded
contexts with CI-enforced boundaries, plus a React web UI, a set of immutable
native pipelines executed in-process, and two small vendored services for
publishing and media.

## System context

```mermaid
flowchart TB
    Owner[Owner / team] --> WEB[Web UI]
    Agent[LLM agents] -- MCP + REST --> API
    WEB --> API[Platform API]
    API --> DB[(PostgreSQL<br/>+ pgvector)]
    API --> REDIS[(Redis)]
    API --> EXT[External providers<br/>LLM · search · images · storage]
    API -- render + deploy --> HUB[Publishing hub]
    HUB --> SITES[Fleet of static sites]
    SITES --> GSC[Search consoles]
    GSC -- daily metrics --> API
```

The API owns all state. External providers are reached only through the
`integrations` context (a single audited channel); published sites are static
properties built and deployed by the publishing hub; search metrics flow back
in on a schedule.

## Containers

```mermaid
flowchart TB
    WEBUI["webui<br/>React 19 + TypeScript, Vite"] --> CONTRACTS["contracts<br/>shared OpenAPI + JSON schemas"]
    CONTRACTS --> Backend
    subgraph Backend["platform — FastAPI, Python 3.12"]
      direction TB
      CTX["Bounded contexts<br/>integrations · runs · research · retrieval · quality · publish<br/>analytics · competitors · cockpit · ops · tickets · tasks · chatlog · specs"]
      NATIVE["Native pipelines<br/>immutable versioned dirs: collect · generate · localize"]
      FLAT["Shared layers<br/>auth · lifecycle engine · ORM · API routers"]
    end
    Backend --> SVC1["wp_deployer<br/>publishing hub service"]
    Backend --> SVC2["imagegen<br/>media generation service"]
```

## The rules that keep it a monolith, not a tangle

1. **A bounded context is a vertical slice** of one business capability —
   its logic, schemas, router and tests live in one folder
   (one package per context inside the backend).
2. **Contexts talk only through contracts.** A context calls another only
   via its public `contracts.py` (typed interface) or an event — never by
   importing internals, never by querying another context's tables. This is
   enforced by an import linter in CI: a violation is a red build, not a
   review comment.
3. **One table — one owning context.** No cross-context SQL joins.
4. **New capability = new module, not new service.** A separate service is
   the exception and needs multiple hard triggers (different runtime,
   secret-scope isolation, independent scaling). The two vendored services
   exist precisely because they met those triggers.
5. **The shared kernel stays thin** — configuration, database, auth
   primitives, logging, event plumbing. No business logic.

The full map of contexts with their responsibilities is on
[Bounded contexts](../contexts/).

## Execution model

There is **no permanently-running orchestrator daemon**. Production runs on a
nightly timer that dispatches the Collect → Generate → publish cycle across
the contract fleet; between runs the platform is idle by design (an
`orchestrator.present: false` status is normal — see
[Interpreting platform status](../../mcp/interpreting-status/)). Every
execution is recorded as a `pipeline_run` with per-step `run_step` rows, so
history is queryable rather than inferred.

Pipelines themselves are **native and in-process**: each version is an
immutable directory of Python steps resolved through the pipeline catalog.
Failures are loud — an error in a step fails the run and is recorded in the
error surface; there is no silent per-step retry (only the LLM adapter
retries transient provider errors internally).
