Architecture — overview¶
Scope of this section: how the system is shaped, and why. System-wide, and it changes rarely.
Two pages, two zoom levels:
- This page — the logical view. Planes, engines, surfaces, and the rules that override convenience.
- Deployment topology — the physical view. Containers, networks, ports and hostnames on an actual host.
- Stack and sizing — every component, what it does, and what it costs in RAM, CPU and disk.
It deliberately does not contain per-product detail. A data model for one product is a recipe over this architecture, not part of it, and lives under Products. If a document only makes sense for one product, it does not belong here.
In one line¶
A shared, stateless brain plus per-tenant isolated stacks, driven by config resolved at runtime, acting on the tools clients already run. Own the spine — state, decisions, memory — and orchestrate the body.
The two planes¶
Everything follows from this split.
flowchart LR
subgraph CP ["Control plane — shared"]
REG[("Registry<br/>tenant records, resolved config,<br/>connector manifests, metadata")]
end
subgraph BR ["The brain — shared, stateless"]
E1["Capture"]
E2["Progress"]
E3["Bill"]
E4["Remember"]
end
subgraph TP ["Tenant plane — one per client"]
T1[("Client A spine")]
T2[("Client B spine")]
end
REG -. "read-only" .-> BR
BR --> T1
BR --> T2
The control plane holds the map, never the content. Tenant records, resolved config, connector manifests, metadata. No emails, no invoices, no client data — ever (ADR-0006).
The tenant plane holds the content. Each client's own Postgres is the spine, plus their own n8n instance. Isolation is the container and volume boundary, not a tenant_id column (ADR-0023).
The brain is shared and stateless. It reads config from the registry and acts on tenant data. It never writes the registry.
Two writers only: Ansible creates the tenant record at bootstrap; the BOS Console edits config. Nothing else (ADR-0006).
Engines vs products¶
Engines are machinery, shared by everyone. A work item flows through them:
flowchart LR
IN(["Inbound<br/>email · form · webhook"]) --> CAP["<b>Capture</b><br/>interpret it"]
CAP --> PROG["<b>Progress</b><br/>work-item<br/>state machine"]
PROG --> BILL["<b>Bill</b><br/>money<br/>state machine"]
REM[("<b>Remember</b><br/>retrieval + memory<br/>pgvector")]
REM -.->|"context"| CAP
REM -.->|"context"| PROG
PROG --> GATE{"AI-generated<br/>action?"}
GATE -->|"yes"| APPROVE["⏸️ Human approval<br/><i>no review, no go</i>"]
GATE -->|"no — deterministic"| ACT(["Execute"])
APPROVE -->|"approved"| ACT
APPROVE -->|"expired"| PROG
Bill runs parallel to Progress, not after it — money and work have separate lifecycles, and conflating them is how a paid-but-incomplete job becomes invisible.
An expired approval re-queues; it never auto-approves (ADR-0026).
| Engine | Does |
|---|---|
| Capture | Interpret inbound — email, form, webhook |
| Progress | Work-item state machine |
| Bill | Money state machine, parallel to Progress |
| Remember | Retrieval and memory, pgvector |
Products are switch-on patterns over those engines — email triage, invoicing. An engine is never built "for" one product or one client (ADR-0002).
flowchart TB
subgraph P ["Products — switch-on patterns"]
P1["Email triage"]
P2["Invoicing"]
P3["…"]
end
subgraph E ["Engines — shared machinery"]
E1["Capture"]
E2["Progress"]
E3["Bill"]
E4["Remember"]
end
P1 --> E1
P1 --> E2
P2 --> E2
P2 --> E3
P3 -.-> E4
This is what makes client specificity config rather than code. New behaviour is a config change; if it genuinely needs new logic, the shared vocabulary is extended so every tenant benefits, then switched on as config (ADR-0003).
Surfaces¶
Who may write what — the arrows are the whole rule:
flowchart LR
OPS(["Operators<br/>El · Lachlan"]) --> CONSOLE["BOS Console"]
CLIENT(["Client users"]) --> APP["Client review app"]
ANS(["Ansible"]) -->|"creates tenant record<br/>at bootstrap"| REG
CONSOLE -->|"edits config"| REG[("Control plane")]
APP -->|"approvals"| TEN[("Own tenant data only")]
APP -.->|"never"| REG
BRAIN["The brain"] -.->|"read-only"| REG
| Surface | Audience | May write |
|---|---|---|
| BOS Console | Operators only | Control plane |
| Client review app | One tenant's users | That tenant's data only |
| Documentation | Us | Nothing — authored in git |
Two writers only to the control plane: Ansible at bootstrap, the Console thereafter. The brain reads it and never writes it (ADR-0006).
The Console is reached through the Internal Portal UI but is a separate service (ADR-0011). The client app never touches the control plane.
Rules that override convenience¶
| Rule | ADR |
|---|---|
| Config, not code, for client specificity | ADR-0003 |
Structured predicates only — no expression language, no eval |
ADR-0004 |
| Rule precedence: guardrails → vertical template → tenant authored → learned | ADR-0005 |
| Control plane holds the map, never client content | ADR-0006 |
| Config applies to new work items only, never retroactively | ADR-0006 |
Data residency is hard — AU infrastructure, region a first-class field |
ADR-0008 |
| Human approval before any AI-generated action executes | ADR-0026 |
Where to go next¶
- Deployment topology — the same system as containers, networks and ports.
- Stack and sizing — what each component is, and what it costs in RAM and disk.
- Decisions — every ADR behind the above.
- Products — recipes over this architecture.