Every API request in Actionary travels the same eight layers. A record read, a workflow enqueue, an MCP tool call, an agent turn — the path from the browser to Postgres and back is the same shape every time. Each layer earns its keep by containing exactly its concern — nothing more, nothing less.
The table below is the substrate the architecture page sits on. Read it as a specification: what each layer is, what it receives, and what it sends down to the next.
The full request chain
| # | Layer | Job (one line) | Receives | Sends down |
|---|---|---|---|---|
| 1 | Browser / MCP client | User interaction, render UI, dispatch agent tool | User click or agent decision | HTTPS request |
| 2 | Reverse proxy | TLS termination, host-based routing (tenant vs staff surfaces) | HTTPS request | HTTP forwarded to the API |
| 3 | API entry + middleware | Auth, CSRF, session validation, tenant resolution, DB role selection, request-id logging | HTTP request | Request with auth context + tenant GUC on DB session |
| 4 | Router | HTTP transport only — parse path / query / body params, dispatch to application service, format response envelope | Request with auth context | Method call to application service |
| 5 | Application service | Business logic — orchestration, policy checks, composition, validation, envelope shaping | Method call from router | Method call to repository |
| 6 | Repository | Data access — SQL statements, tenant scoping baked into every query, no policy or composition | Method call from service | SQL string + params to DB connection |
| 7 | Connection tier | Pool management, role acquisition, per-session tenant setting on the database connection | SQL + params from repository | Query execution |
| 8 | Postgres substrate | Executes SQL under row-level-security policies keyed on the tenant setting | SQL statement | Result rows or exception |
Return path is the same layers in reverse: Postgres → connection → repository (rows → dicts) → service (dicts → domain shape) → router (domain shape → envelope) → middleware (envelope → JSON) → proxy → browser.
Eight rules — one per boundary, top to bottom
Every layer has one rule. Each rule is a single-sentence invariant plus a one-sentence ban on the shape that violates it. When code violates the rule, the layer boundary the rule protects is not real.
- Layer 1 — Client holds no trust. The browser or MCP client renders and dispatches; it never enforces auth or tenancy. Anything the client asserts is re-checked server-side.
- Layer 2 — The proxy routes, it does not reason. Host maps to tenant or staff surface and TLS terminates here. If routing logic starts inspecting bodies or making auth decisions, it is in the wrong layer.
- Layer 3 — Security lives in middleware. Auth, CSRF, session validation, tenant resolution, and DB role selection happen once, here. If a router or service re-derives identity or tenancy, that logic is misplaced.
- Layer 4 — Routers do HTTP transport only. Parse params in, shape the envelope out, dispatch to a service. If a router does composition, orchestration, or envelope shaping beyond formatting, it is in the wrong layer.
- Layer 5 — Business logic only in services. Orchestration, policy checks, composition, and validation belong here. A service must not write raw SQL, and must not know about HTTP.
- Layer 6 — SQL only in repositories. Every query lives here with tenant scoping baked in. If SQL appears in a service or router, it is in the wrong layer; if policy or composition appears in a repository, that is misplaced too.
- Layer 7 — The connection tier owns roles and the tenant setting. Role acquisition and the per-session tenant setting happen here, in one place. Repositories receive a scoped session; they never pick roles themselves.
- Layer 8 — Postgres is the last line, not the only line. Row-level-security keyed on the tenant setting enforces isolation even if a query above forgets its filter. A missed scope is a performance issue here, never a disclosure.
Where each rule descends from
Every rule above traces to a paper on the design-principles reading list. Actionary did not invent these ideas — it applies them consistently.
| Rule | Descends from | Year |
|---|---|---|
| Client holds no trust | Saltzer & Schroeder, The Protection of Information in Computer Systems — least privilege at the boundary | 1975 |
| The proxy routes, it does not reason | Dijkstra, On the role of scientific thought — separation of concerns | 1974 |
| Security lives in middleware | NIST SP 800-207 Zero Trust Architecture — policy enforcement point at the boundary, once per request | 2020 |
| Routers do HTTP transport only | Fowler, Patterns of Enterprise Application Architecture — the Service Layer chapter, applied at the transport edge | 2002 |
| Business logic only in services | Fowler, PoEAA — Service Layer, plus Martin, Clean Architecture — use-case interactors | 2002 / 2017 |
| SQL only in repositories | Fowler, PoEAA — Repository pattern, plus Evans, Domain-Driven Design — Repository from §5 | 2002 / 2003 |
| Connection tier owns roles and the tenant setting | Saltzer & Schroeder — least privilege applied at the connection tier, with role assumption factored to one location | 1975 |
| Postgres is the last line, not the only line | Postgres row-level security (2016) applied as defence in depth per NIST SP 800-53 SC-3 | 2016 |
The “cool mapping” is the point. Every rule descends from a citation on the fifty-year reading list. Fifty years of production experience compressed into eight sentences.
Nine CI-enforced contracts prove the rules
Every merge runs nine assertions against the code. The rules above are stated in English; the assertions below run at merge time and fail the build when the shape they protect drifts. Every file declares which layer it belongs to on its first line, so the checks stay accurate as the tree evolves.
- Inline-SQL budget — SELECT, INSERT, UPDATE, DELETE only appear in repository files. Inline SQL in a router or service fails the build. Proves Layer 6 — SQL only in repositories.
- No repository imports from routers — a router file cannot import from a repository module. Every data-access dependency routes through an application service. Proves Layer 4 — Routers do HTTP transport only.
- No web framework in services — application service files stay transport-agnostic. The same method serves the HTTP router, an MCP tool call, a workflow node, and a background worker. Proves Layer 5 — Business logic only in services.
- No direct database access in services — the raw database connection is unreachable from the application layer. It only appears through the connection tier. Proves Layer 5 → Layer 7 boundary.
- No database driver outside the connection tier — connection primitives live in one place. Value types and error classes stay importable, but the driver’s connection object never appears above persistence. Proves Layer 7 — Connection tier owns roles.
- Repository aggregate naming — every repository is named after its aggregate; two repositories may not write to the same table. One aggregate — one repository. Proves Layer 6 — Repository pattern applied consistently.
- No module function on instance and no double module reference — two structural checks that catch the naming-collision bug class introduced by class-to-module repository conversions. Deterministic sub-second sweeps at merge time.
- SPA feature-slice isolation — every feature slice in the client is private; cross-slice imports resolve to a shared kernel or a per-file composition allowlist. Proves Layer 1 — Feature-Sliced Design applied to the client.
- Platform-admin authorization — every staff-platform route gates on the staff-platform session kind. A boolean superuser flag is banned on any table, payload, JWT claim, or in-memory user object. Proves the SOC 2 CC6.1 invariant.
Every one of the nine has a stated invariant, the allowlist mechanism where one exists, and a firm exit-code contract. A refactor that regresses the shape cannot ship.
One Voice — the discipline the layers serve
The eight layers are the structural half of a broader engineering rule Actionary calls One Voice: every file in the codebase reads as if the same lead engineer wrote it. Layering says what belongs where; One Voice says what shape it takes once it gets there. One Generic Repository handles every entity, driven by the entity metadata layer. One naming convention across the tree. One error-handling shape per concern. One place for SQL. One SPA-to-API contract, verified against the OpenAPI schema at every merge. One typed SDK, generated from that schema. One authentication contract — a session-kind string with three values, no boolean flag anywhere in the system that flips to elevate. One comment discipline: present state only, no fix history in code. One migration shape, with -- phase: declared and expand/contract sequenced.
The purpose is straightforward. A codebase with One Voice can be handed to a new lead engineer without a two-quarter ramp. A codebase without it forces every new hire to learn seven variations of every pattern before they can safely change anything — the cost is paid on every hire, every audit, every acquisition due-diligence read, and every incident-response session at three in the morning when the on-call engineer has to reason about code they have not seen before under time pressure.
Google, Airbnb, Uber, Meta, and the Rust ecosystem all run on this bar. The Actionary house discipline is written down as a repo-wide arc — nine passes, substrate first, one commit per subsystem, the same ten-question audit applied to each chunk. Style consistency treated as on-going engineering practice, not one-off cleanup.
The takeaway
Clean architecture in most codebases is an aspiration a lead engineer defends in code review. Here it is mechanically un-regressable. The eight layers are drawn. Each layer has one rule. Nine merge-time checks read the layer marker on every file and run the rules against the code. One Voice is the discipline they collectively enforce.
Read the architecture page for the substrate the layers run on. Read the design principles for the fifty-year reading list every rule descends from — One Voice is principle sixteen. Read the open-standards stance for what stays portable at every boundary the layers cross.