Architecture
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
Section titled “System context”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
Section titled “Containers”The rules that keep it a monolith, not a tangle
Section titled “The rules that keep it a monolith, not a tangle”- A bounded context is a vertical slice of one business capability —
its logic, schemas, router and tests live in one folder
(
platform/app/contexts/<name>/). - 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. - One table — one owning context. No cross-context SQL joins.
- 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.
- 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.
Execution model
Section titled “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). 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).