Skip to content

Stack and sizing

What each component is, why it is here, and what it costs in resources. Use the sizing worksheet when provisioning a host.

These are planning figures, not measurements

Every number below is a starting estimate for sizing a box. The Metabase figure in particular is unverified — confirming it is OI-002, and it is the single largest line. Measure on the first host and correct this page.


The layers

flowchart TB
    subgraph HOST ["Host — Ubuntu 24.04 LTS"]
        direction TB
        subgraph EDGE ["Edge"]
            CADDY["Caddy"]
        end
        subgraph SHARED ["Core stack — once per host"]
            BRAIN["LangGraph brain"]
            CONSOLE["BOS Console"]
            REGDB[("Registry Postgres")]
        end
        subgraph TENANT ["Tenant stack — once per client"]
            APP["Client review app"]
            TDB[("Tenant Postgres<br/>+ pgvector")]
            N8N["n8n"]
            NDB[("n8n Postgres")]
            MB["Metabase<br/><i>optional</i>"]
        end
    end

Core stack — paid once per host

Two different scopes hide in here

The registry and Console exist exactly once globally, however many hosts you run. Caddy and the brain exist once per host. See ADR-0030 — on a second box you add another Caddy and another brain replica, never another registry.

Component Scope What it does Why this one RAM Notes
Ubuntu 24.04 LTS Per host Operating system Long support runway; standard for Ansible and Docker ~400 MB Socket-activates SSH — see the host setup runbook
Docker + Compose Per host Container runtime and per-stack orchestration Isolation without an orchestration platform to operate. Not Kubernetes (ADR-0007) ~150 MB Daemon overhead, excluding containers
Caddy Per host Reverse proxy, automatic TLS, hostname and path routing Automatic certificates via the Cloudflare DNS challenge; far less config than nginx ~50 MB The only public door: ports 80 and 443
LangGraph brain Per host (replica) The shared engines — Capture, Progress, Bill, Remember Stateless and shared, so every tenant benefits from one change (ADR-0002) ~750 MB Python. Holds no state — config from the registry, checkpoints to tenant Postgres
Registry Postgres Global — exactly one Control plane: tenant records, resolved config, connector manifests The map, never client content (ADR-0006) ~256 MB Small. Its own isolated network. Never run two
BOS Console Global — exactly one Operator surface — the only thing that edits control-plane config Separate service from the Internal Portal (ADR-0011) ~256 MB Serves /health monitoring on a subpath

Per host, if it carries the control plane: ~1.9 GB including the OS. A host without the control plane: ~1.35 GB — no registry, no Console.


Tenant stack — paid per client

Component What it does Why this one RAM Notes
Tenant Postgres The client's spine — work items, approvals, landing/modelled/serving layers One instance per tenant. Isolation is the container and volume boundary, not a tenant_id column (ADR-0023) ~512 MB pgvector image, pinned major version. Largest disk consumer
n8n Connectors, credentials, triggers for that client's tools One instance per tenant. Projects feature not used — licence compliance (ADR-0023 §4) ~400 MB Spikes during workflow execution
n8n Postgres n8n's own state, including encrypted OAuth tokens Kept separate from the client's business data ~256 MB Small and stable
Client review app Approvals inbox and history, headline metrics Reads its own tenant's data only; never the control plane ~256 MB Static-ish; light
Metabase Self-serve dashboards at /insights Only for clients who bought the dashboard product (ADR-0023 §5) ~2 GB JVM. The dominant cost — see the warning above

Per tenant: ~1.4 GB without dashboards, ~3.4 GB with.


Sizing worksheet

Core stack (incl. OS)                    1.9 GB
+ automation-only tenants   × 1.4 GB
+ dashboard tenants         × 3.4 GB
+ headroom (25%)
─────────────────────────────────────────────
= provision at least this much RAM
Scenario Calculation Provision
Client-zero only 1.9 + 1.4 + 25% 4 GB
3 automation tenants 1.9 + 4.2 + 25% 8 GB
3 automation + 1 dashboard 1.9 + 4.2 + 3.4 + 25% 12 GB
5 automation + 2 dashboard 1.9 + 7.0 + 6.8 + 25% 20 GB

Dashboard clients, not automation clients, drive the size. One Metabase costs more than two entire automation tenants — which is why it is conditional on the tenant's services list and priced into the dashboard product.

CPU

2 vCPU is enough to start. This is not a compute-bound workload — it waits on APIs and databases. Add cores when tenant count grows, not when latency feels slow; latency is almost always an upstream API.

GPU

None. Ever, on this box. Inference is an API call to Anthropic or Gemini via packages/llm-client (ADR-0009). If self-hosted inference is ever adopted, it gets its own machine — never the host serving client data.

Disk

Item Estimate
OS, Docker, images ~20 GB
Per tenant, first year ~5–20 GB
Landing layer Grows and is never pruned — kept permanently so metric redefinitions rebuild locally instead of re-pulling against the AroFlo API cap

Start at 80 GB; monitor. Block storage is the later option rather than resizing the instance.


Off-box, and deliberately so

Component What it does Why off-box
Cloudflare Pages This documentation site A runbook must survive the outage it is used in (ADR-0028)
Cloudflare DNS + Access Routing and identity Independent of the host
Vultr AU object storage Encrypted backups A backup on the host it protects is not a backup (ADR-0026)
Sentry Error alerting Reports errors the box cannot report itself
External uptime check Is it alive at all An on-box monitor cannot tell you the box is down (OI-048)
Anthropic / Gemini LLM inference No model weights, no GPU, no inference on this host (ADR-0009)
Bitwarden Human credentials Availability independent of our infrastructure (ADR-0027)