Skip to content

Layered slices

This page is the rulebook: the 8 groups, the one dependency rule, and the mental models that decide where new code goes. For the full list of every sub-slice, see the Slice breakdown; for why the api is shaped this way at all, start at the Overview.

The 8 groups

Inside api/src/slices/, every CleanSlice vertical slice belongs to one of 8 groups, ordered from foundation to top:

#GroupOne job
L0infraadapters to external systems (DB · cache · blob · vector) — pure I/O drivers, no domain
L0setupframework & exposure plumbing (core = REST, mcp = MCP) — no capability of its own
L1systemshared services that each do something: meter, gate, notify, infer, store files
L2useridentity & tenancy — the tenant is the team
L3adminops & audit; reads other groups' data via Prisma, imports nothing above
L4runtimethe ephemeral execution pipeline: queue → pod → event stream
L5agentthe agent domain and the brain (orchestrator)
L6billingthe top sink — money; nothing imports it

The one rule

Dependencies point only downward. No cycles. A group may import only groups below it.

infra   ← nobody
setup   ← (at most) infra
system  ← infra · setup
user    ← system · setup · infra
admin   ← user (+ reads the rest via Prisma)
runtime ← system · setup · infra            (no edges into agent)
agent   ← runtime · system · user · …       (the brain reaches down to all)
billing ← system · user · notification      (TOP: nothing imports billing)

This invariant is enforced in CI (a boundary check fails the build on an upward import — see Implementation). Everything else on this page exists to answer one question: which group does my new slice belong to?

Which group? — the mental models

infra vs setup vs system:

  • infra = adapters to external systems (DB, cache, blob, vector). Pure I/O drivers.
  • setup = framework / exposure plumbing with no capability of its own — it exposes other slices' functionality. core = REST exposure, mcp = MCP exposure.
  • system = shared L1 services that each do something (record / gate / notify / infer / store).

system vs runtime:

  • system = state & capability (what's true / foundational). Persistent, low.
  • runtime = mechanism & action (how a task runs): queue → ephemeral pod → stream.

The system membership test: an L1 leaf (deps only on infra/setup) used by multiple higher layers. Keep it strict, or system re-accretes into a fat catch-all.

Placement rulings (don't re-litigate without reason)

  • orchestrator is the brain → lives in agent, not runtime. Runtime = pure mechanisms; the brain reads agent-domain state and drives the mechanisms, so it sits above them.
  • billing is a pure sink → top (L6). Nothing imports billing; setting reads team.planId (which billing writes) instead of importing billing (dependency inversion).
  • llm and file are in system, not runtime — foundational capabilities used by many layers, not part of the execution pipeline. ("Emits usage" is not a colocation criterion.)
  • mcp stays in setup, not system — it's a transport that exposes other slices' tools (twin of core's REST), with no capability of its own.
  • admin stays low (L3) by reading other slices' data directly via Prisma (no feature-module imports), so it doesn't drag agent/runtime deps into a low layer.
How we got here (1.x → 2.0 renames)

platform was renamed system. The old fat "account" group was dissolved: user, admin, billing became their own groups; usage/setting/notification/llm/file are the shared L1 services. publicApi was dissolved (rate-limit → core, webhook delivery → notification, keys already in user/apiKey). runtime/artifacts folded into system/file.

See also

  • Slice breakdown — every sub-slice in every group, one table each.
  • Overview — why the api is one image with two roles.
  • Implementation — the build prompt that wires the groups + the CI boundary check.
  • Conventions — the CleanSlice rules inside a slice (domain/data/dtos, gateways).