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:
| # | Group | One job |
|---|---|---|
| L0 | infra | adapters to external systems (DB · cache · blob · vector) — pure I/O drivers, no domain |
| L0 | setup | framework & exposure plumbing (core = REST, mcp = MCP) — no capability of its own |
| L1 | system | shared services that each do something: meter, gate, notify, infer, store files |
| L2 | user | identity & tenancy — the tenant is the team |
| L3 | admin | ops & audit; reads other groups' data via Prisma, imports nothing above |
| L4 | runtime | the ephemeral execution pipeline: queue → pod → event stream |
| L5 | agent | the agent domain and the brain (orchestrator) |
| L6 | billing | the 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)
orchestratoris the brain → lives inagent, notruntime. Runtime = pure mechanisms; the brain reads agent-domain state and drives the mechanisms, so it sits above them.billingis a pure sink → top (L6). Nothing imports billing;settingreadsteam.planId(which billing writes) instead of importing billing (dependency inversion).llmandfileare insystem, notruntime— foundational capabilities used by many layers, not part of the execution pipeline. ("Emits usage" is not a colocation criterion.)mcpstays insetup, notsystem— it's a transport that exposes other slices' tools (twin ofcore's REST), with no capability of its own.adminstays 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
apiis 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).