ADR-0037: BOS Console: scope, operating principle and data boundary¶
- Status: Accepted. §4 superseded in part by ADR-0041 (a masked view of the corrections behind a count is shown in place, fetched live and never stored)
- Date: 2026-10-11
- Deciders: El
- Supersedes: ADR-0011 in part: the Console now shows and changes, not only edits, and is its own application at its own hostname rather than reached through the Internal Portal UI. The client review app half of ADR-0011 stands.
- Relates to: ADR-0006, ADR-0029, ADR-0034, ADR-0038, ADR-0039, ADR-0040
Context¶
Running ABOS today means remembering things. Setting up a Junior mailbox is a manual trigger in n8n; clearing what testing taught it is a form in n8n; publishing is a button in n8n; provisioning is ansible-playbook on octo-ctl with the right --limit; checking that a mailbox is healthy means reading data tables and execution lists. Each is written down, and each depends on someone remembering where it is written. At two operators that already costs time, and every new tenant multiplies it.
ADR-0011 defined the Console as the editor of the control plane. It said nothing about seeing the fleet, running operations, or where the line sits between what the Console may know and what stays in a tenant. Those three are now forced by the Junior: it has settings to own (OI-095), health to watch, and routine jobs that should not need SSH.
Decision¶
flowchart LR
OPS(["El, Lachlan"]) -->|"Cloudflare Access, Entra SSO"| CON["BOS Console<br/>shows and changes"]
CON --> REG[("Registry<br/>settings, aggregates,<br/>history, jobs")]
TL["Tenant link<br/>in each tenant's n8n"] -->|"pulls settings and jobs<br/>pushes aggregates"| REG
RUN["Runner<br/>on octo-ctl"] -->|"pulls approved jobs<br/>reports output"| REG
RUN -->|"playbooks from main"| HOSTS["Hosts"]
1. The Console is the main place ABOS is run¶
Nobody should need to remember SSH or Ansible commands for routine work. SSH and the octo-ctl command line are break-glass only.
The rule: anything done by hand on octo-ctl, or by hand in a tenant's n8n, more than once becomes a Console job. The job menu grows by this rule and no other way (ADR-0039).
Break-glass runbooks stay in the docs (ADR-0028) and the Console links to each one, so they are findable from the Console in normal times and readable without it when the Console itself is the problem. The docs site does not depend on the box (ADR-0029 §4); the Console does.
2. Operators only, generic model, Junior first¶
| Who | El and Lachlan, both admins. No client logins, no roles below admin in v1 |
| What v1 serves | The Junior: tenants, its mailboxes, their settings, health and alerts, and the jobs that provision them |
| The model | Generic: tenant, then product (Junior, later Senior), then mailbox or other connected account. Senior fits without a schema rewrite |
3. The Console shows and changes¶
It shows the fleet's state and changes it: settings, tenant-side actions (such as resetting what a mailbox has learned) and provisioning jobs.
Client-facing reporting stays separate. What a client sees about their own business is the review app and the reporting design (draft reporting ADR), served from their own tenant. The Console is ours.
4. The data boundary: settings and aggregates only¶
| Crosses into the registry | Never crosses |
|---|---|
| Settings and their history | Email content, subjects, snippets |
| Counts: emails sorted by bucket and by rule, per mailbox per period | Sender addresses, recipient addresses, display names |
| Correction counts by type, and the correction rate | Message IDs and thread IDs |
| Poll health: last completed poll, poll gaps, history resets | Anything from known_senders, sort_log rows or corrections rows |
| Alert state: open, resolved, kind, rule | Credentials of any kind |
| Job requests, approvals, results and output |
Detail means opening the tenant's own n8n, over the tailnet, as today. The Console may link to it; it never copies from it. This is ADR-0006's "map, not content" applied to everything the Console shows, and it is the reason the boundary is a list of allowed aggregates rather than a list of forbidden fields: anything not on the allowed list does not cross.
5. Validation and change history from day one¶
- One settings schema, defined once, validated by the Console API on every write. A value the schema does not know is refused, never stored and silently ignored (closes OI-062 when built).
- Every change, action and job is recorded: who, when, what, before and after. The history is append-only and the Console never edits it (closes OI-063 when built).
Neither is added later. A Console that wrote unvalidated, unrecorded settings for a month would leave exactly the gap these two items describe.
6. Stack and deployment¶
| Back end | FastAPI, the same as the Internal Portal |
| Store | The control-plane Postgres, the registry (ADR-0025 §2) |
| Front end | React with Vite and TypeScript |
| Where the code lives | This repository, as a separate service |
| How it ships | Ansible, never a git push (ADR-0029 §2) |
| How it is reached | ADR-0040 |
Consequences¶
Routine work becomes clicks with a record behind every one, and the record answers "who changed this, and what was it before" without anyone's memory.
The boundary keeps the registry safe to hold centrally: a leak of the registry exposes how many emails a client received and how often they corrected us, never what anyone wrote. The cost is that an operator investigating one email must open the tenant's n8n; that is deliberate.
The job rule means the Console's surface grows with what operators actually repeat, rather than with what seemed useful when it was designed. A task done twice by hand is a missing job, and should be raised as one.
Building validation and history first makes the first phase slower and every later phase safer.
Alternatives considered¶
- Keep running from SSH, Ansible and n8n, with better runbooks is rejected: it scales with operator memory, and every tenant multiplies it.
- Copy row-level detail into the registry for convenience is rejected: it breaks ADR-0006 and turns the registry into the most valuable thing to steal.
- A Console that only shows, with changes still by hand is rejected: the change is where mistakes happen, so it is where validation and history matter most.
- Building the Console inside the Internal Portal is rejected for the reasons ADR-0011 gave: different isolation domains, and the portal must not own control-plane writes.
- Adding validation and history after v1 is rejected: the first month of unrecorded changes is the month that gets disputed.