What Bella splits, what it deliberately does not split, and what broke when Compose guarantees were lost
This lesson has been refreshed against the latest public reference: MetatechID/bella-enterprise main at dc42148. The important update is that Bella is no longer best described as “six containers plus local Postgres and Redis.” The current shape is a containerized application tier backed by external managed stores, with a larger and more explicit ADR trail.
Bella runs Web, API, Worker, WhatsApp Bridge, Hindsight, optional Caddy/OTel, a wiki-dashboard, and deployment/reconciliation jobs as Dockerized services. Postgres, Redis, and MongoDB are external env-only stores via DATABASE_URL, REDIS_URL, LOGS_REDIS_URL, and MONGO_URL. The repo now contains 80 ADR files and the Drizzle schema has 81 table declarations.
Service decomposition is not “make everything a microservice.” A boundary earns its cost when the two sides have different trust levels, runtime requirements, failure modes, or operator lifecycles. Bella's current decomposition is mostly about those runtime boundaries. Its data layer is intentionally not decomposed into one database per service.
The pattern is:
Production Web serves on :3000 inside the container; 5173 is a host/debug mapping and Vite dev port, not the production runtime. Support services such as wiki-dashboard, caddy, otel-collector, and migrate/reconciliation jobs exist in the deployment but are kept out of this main-path diagram so the boundary lesson stays readable on mobile.
The previous version described web as “React/Vite :5173” and listed Postgres/Redis/logs-redis as in-stack containers. The latest reference says production web is a static Node server on :3000, while Postgres, Redis, Log Buffer Redis, and MongoDB are external managed services reached by env URLs.
Not every container deserves a new product boundary in the main diagram. The latest compose reference also includes these support services:
wiki-dashboard — internal dashboard on host :8000. Operational UI, not a new reasoning boundary.caddy — profile-gated edge on :80/:443. Self-hosted ingress option alongside Traefik/Dokploy deployments.otel-collector — OpenTelemetry collector. Observability plumbing, not a product data owner.migrate / reconciliation jobs — schema and data convergence. Important operator lifecycle, but superseded in per-service production by boot-intrinsic reconciliation.The Web service is a React SPA served by a Node static server. It talks to the API using relative routes like /api, /auth, /chat, /setup, and /health/*. The Web container joins the public routing network so Traefik/Dokploy can reach it, while it proxies application calls to http://api:3000 internally.
This boundary is conventional but still earned:
Split presentation from capability execution, but keep URLs relative when the app and API are one product surface. That preserves portability across Dokploy, Caddy, and local dev without rebuilding the SPA for every domain.
This is Bella's most important boundary. The Worker is the only process allowed to spawn or speak to a reasoning engine. The API can authenticate, persist, validate, and forward, but it must not shortcut into Anthropic/OpenAI/Claude SDK calls.
// Forbidden outside the Worker
❌ import Anthropic from '@anthropic-ai/sdk'
❌ anthropic.messages.create(...)
❌ import { query } from '@anthropic-ai/claude-agent-sdk'
The updated nuance: the default engine is no longer called “Bella brain.” The roster is now claude-code, hermes, pi, nullclaw, and nullclaw-lean. ADR 0063 removes remote-hermes; per-tenant engine containers are gone because no org used that path and it added provisioning complexity.
Engine selection is per-org, but every engine enters through the Worker seam and reaches tools through the Gateway surface.
Why this boundary exists:
claude-code.The WhatsApp Bridge remains a Go service built on whatsmeow. The rest of Bella is TypeScript. This split is not aesthetic; it is a protocol-library decision.
ADR 0007 rejected a TypeScript/Baileys rewrite. The fix for protocol drift is updating whatsmeow, not rewriting the bridge.
The lesson: language mismatch is cheaper than protocol uncertainty. A rewrite would remove one boundary but replace it with new failure modes in the most brittle part of the product: WhatsApp connectivity.
The old lesson described two Redis containers. The current reference changes the deployment shape, not the design pattern. Bella still separates app queues/cache from diagnostic log buffering, but those Redis instances are now external env-only stores:
| Store | Env var | Purpose | Failure posture |
|---|---|---|---|
| App Redis | REDIS_URL | BullMQ queues, scheduled jobs, cache | Required; API/Worker fail fast if missing |
| Log Buffer Redis | LOGS_REDIS_URL | Raw log stream for Superadmin Log Console | Optional; readers degrade when unset |
This still implements the bulkhead pattern. The diagnostic firehose should not evict queue/session keys or OOM the app store. ADR 0019 originally framed this as a dedicated logs-redis container; the current compose comments say to provision equivalent isolation externally — a second instance, logical DB, or namespace with bounded eviction.
Hindsight is Bella's org-memory store. The older lesson called out a stale deployment bug: the Worker could not reach Hindsight because HINDSIGHT_URL was missing. In the current canonical compose files, both API and Worker pass HINDSIGHT_URL=http://hindsight:8888.
The latest problem moved up one level: Hindsight must work both in a single Compose project and in per-service Dokploy Applications. ADR 0059 turns it into a thin Bella-wrapped application that fetches LLM credentials from an internal API endpoint at boot instead of depending on a shared bella_config volume. That makes credential edits DB-backed and panel-driven.
A service boundary is incomplete until configuration and credentials follow the same deployment model. A container that only works when a hidden shared volume happens to exist is not really portable across deployment modes.
This is the best system-design lesson in the updated reference. A single Docker Compose project gives Bella four guarantees “for free”:
api, worker, wa-bridge, hindsight resolve on one network.bella_config, Hindsight data, secrets, and tenant directories are the same volume.ADR 0058 documents what broke when the operator split Bella into separate Dokploy Applications: service names stopped resolving, volumes with the same name were not actually the same volume, cross-app depends_on disappeared, and per-app env drift became possible. This caused real silent failures: Hindsight recorded zero memories, Redis queues could no-op, and encryption-key drift could make Worker config undecryptable.
The newer ADRs shift the lesson from “use a migrate container” to “make reconciliation intrinsic to every deployment mode.”
ADR 0067 then refines the answer: do not rely on a fragile, hand-ordered migrate app for every reconciliation duty. Postgres DDL should ride boot-time runMigrations(); Mongo indexes should ride the first-connect hook; heavy backfills should run after health with advisory locks and version sentinels. The deployment should converge on both fresh installs and updates without a human SSHing in.
| Boundary | Current shape | Why it exists | Reference |
|---|---|---|---|
| Web ↔ API | Static React server :3000 ↔ Hono :3000 | Presentation vs authenticated product API | README.md, TECHNICAL.md |
| API ↔ Worker | API forwards turns; Worker owns engines | Credential isolation, auditability, engine selection | CLAUDE.md, ADR 0021, ADR 0042 |
| Worker ↔ Bridge | TypeScript Worker ↔ Go whatsmeow bridge | Protocol library quality beats language uniformity | ADR 0007 |
| App Redis ↔ Log Redis | External REDIS_URL and LOGS_REDIS_URL | Bulkhead between queues and diagnostic firehose | ADR 0019, compose comments |
| Worker ↔ Hindsight | Org memory service :8888, host :8100 | Shared long-term memory and recall across engines | ADR 0010, ADR 0059 |
| Postgres ↔ MongoDB | Operational state in Postgres; transcript + executions + outcomes in Mongo | Conversation history/audit/metrics are document-shaped and org-standard | ADR 0048, ADR 0049, ADR 0053 |
| Compose ↔ per-service apps | Same services, different operator guarantees | Per-service deployments lose implicit DNS/volume/env/order | ADR 0058, ADR 0067 |
| Support services | wiki-dashboard, caddy, otel-collector, migrate/reconciliation jobs | Operational surfaces and deployment plumbing should not be confused with core product boundaries | docker-compose.yml, ADR 0067 |
| Tenant isolation | Proposed app-layer seam plus Postgres RLS backstop | Forgotten org predicates must fail closed | ADR 0077 |
Use this order. It is more useful than “microservice or monolith?”
The dangerous move is not “one service” or “many services.” The dangerous move is relying on hidden platform guarantees. Compose gave Bella DNS, ordering, shared volumes, and env identity structurally. Per-service Dokploy apps do not. If the architecture diagram still assumes those guarantees, the diagram is lying.
The next lesson should cover Bella's Action Gateway: why ADR 0010 pushed engines toward a shared typed action surface, how the catalog grew past 100 verbs, and why http.request remains a controlled escape hatch instead of giving every engine raw API access.
Read before the next lesson: ADR 0010 — Stateful invertibility and Gateway convergence.