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) |