Skip to content

Data model — Tenant · Principal · Email-Triage

Status: Draft · supports ADR-0017 (employees as principals) · Date: 2026-07-31 Diagram: docs/architecture/diagrams/abos-er.svg (source: abos-er.dot)

This document is the narrative companion to the ER diagram. It describes how an employee (Principal) and a triaged email are represented across the two planes, and — critically — which relationships are enforced foreign keys and which are logical references across a database boundary.

The two planes

Control plane — shared registry. Two writers only (Ansible bootstrap, BOS Console edits); the brain reads it read-only (control-plane-holds-the-map ADR, ≈0006 — verify number). Holds the map, not business content: Tenant, Role, Principal, MailboxBinding, Rule.

Tenant data plane — per-tenant Postgres, inside the tenant's own container (ADR-0014 §5–6). Holds business content, every row stamped principal_id, with RLS enforcing cross-employee isolation (ADR-0017). This plane goes with the client on exit (residency/exit ADR, ≈0008). Holds: Message, Classification, Draft, TriageFeedback, GenerationFeedback, VoiceExample, LearnedRuleCandidate.

Enforced FKs vs logical cross-plane references

The distinction the diagram encodes with solid vs dashed edges is load-bearing:

  • Solid edges = real foreign keys within one database.
  • Dashed edges = logical references that cross the plane boundary. Message.principal_id and Draft.principal_idPrincipal, and Classification.matched_rule_idRule, are not enforced DB foreign keys, because the registry and the tenant Postgres are separate databases in separate containers. The brain resolves them at runtime. Modelling them as enforced FKs would misrepresent the isolation architecture.

Consequently the registry's tenant → connection-string resolution is security-critical: a wrong mapping is a cross-tenant leak.

Entities of note

  • Principal — an employee, or a shared mailbox (kind = person | shared). Carries role_id, autonomy_level (L1 default), and its mailbox binding. Onboarding = create + assign role (inherits ruleset) + delegated OAuth consent.
  • Rule — polymorphic scope_type (system | vertical | tenant | role | personal | learned) with nullable scope_ref. Lives in the control plane because rules are config. Precedence resolves in that order.
  • Draft — the approval queue is the set of status = pending rows; there is no separate queue table (DB-first, clean workflow exit). approver_principal_id is a pointer: self-review by default, re-routable to a manager/rota without a schema change.
  • ClassificationTriageFeedback trains the shared role layer. DraftGenerationFeedback trains only that principal's voice. The two channels are never conflated.
  • VoiceExample — pgvector, per-principal, never pooled across employees.
  • LearnedRuleCandidate — evidence aggregated in the tenant plane; promoted to an active Rule in the registry only on human approval (the "No review, no go" gate; also the insertion point for the curator / SkillOpt per ADR-0019).

Runtime path for one email

  1. A Graph change-notification (one subscription per employee mailbox) fires; the subscription id resolves to (tenant, principal) via the registry and dispatches into that tenant's pipeline.
  2. Capture pulls the message with the principal's token, composes the effective ruleset (system → vertical → tenant → role → personal → learned), classifies, writes a principal-scoped row, decides promote / notify / ignore.
  3. On promote, Generate retrieves only that principal's VoiceExamples, drafts via llm-client, writes the Draft immediately (DB-first), status pending.
  4. Gate: the principal's autonomy_level decides — L1 → queue; higher → auto-send only if the client lifted that gate, with the authorisation recorded. Workflow terminates cleanly; the pending row is the queue.
  5. Feedback on approve/edit splits: triage signal → role layer; edit-diff → that principal's voice corpus. Retrieval, not fine-tuning.

Open items (tracked)

  • RLS session-principal mechanics: how the brain sets the Postgres session context per invocation so RLS binds.
  • Graph subscription lifecycle: renewal and offboarding revocation.
  • Confirm tenant compose layout: is the tenant's n8n in the same compose project / isolated network as its Postgres?