The example application's architecture¶
How the enterprise example is put together, and why each layer is where it is. The library's own
../architecture.md describes the library; this page describes one
application that uses it.
Two ASGI applications, deliberately¶
outer app (examples/enterprise/app/main.py:create_app)
├── /healthz liveness -- no credentials, no dependencies
├── /readyz readiness -- control plane statement + a policy decision
└── /api ──mount──▶ tenant app (install(...): middleware, guard, envelope)
├── /whoami require("tenant:read")
├── /tenants/current require("tenant:read")
└── /resources... require("resource:read" | "resource:write")
The tenant plane is mounted under /api; nothing under it is reachable without a verified principal
and a resolved tenant. The probes are outside it, because a probe that needs a token fails during an
identity-provider outage and gets the process restarted for it.
One composition, two surfaces¶
build_collaborators() builds every collaborator once; create_app(collaborators) serves HTTP
and build_example_mcp_server(...) serves MCP from the same objects. The unit tests assert that by
identity, not by shape — a second authenticator or a second enforcer would be a second place for
a decision to drift, and no test would notice.
Collaborators (frozen dataclass)
├── settings, config configuration, validated at startup
├── control_engine, sessions the control plane's connection
├── audit, security_sink the platform trail + the security-event join
├── reader, registry access reads, tenant lifecycle
├── plane, fence, resources the tenant plane + the write fence + the service
├── pep, enforcer, container policy engine, the bridge, the FastAPI container
├── principal_provider, chain, factory authentication → tenant resolution → context
└── graph the derived plane, when configured
The layers, and what each one may decide¶
| Layer | Decides | May not |
|---|---|---|
app/configuration/ |
which settings exist, which are required | hold a secret |
app/security/ |
which collaborator answers each question | decide policy |
app/api/ |
which permission a route needs | query, or catch a typed error into a new shape |
app/services/ |
the transaction boundary, the audit of a write | authorize, or resolve a tenant |
app/repositories/ |
which statements run | commit, or open its own session |
app/models/ |
the schema the application owns | know about HTTP |
app/mcp/ |
the tool inventory, the schemas | build its own collaborators |
app/dataplane.py |
the plane, RLS, the operator grants | serve a request |
app/bootstrap.py |
the operator steps (tenant, schema) | run at request time |
Where the seams are¶
Every security mechanism is the library's: install (middleware + guard + envelope),
require(...) (the declared permission), build_authenticator, the ResolverChain, the
ContextFactory, AuthorizationPEP through CerbosEnforcer, SecurityEventEmitter,
GraphRepository and GraphRunner, build_mcp_server and its boundary. The example supplies the
choices — which engine, which directory, which sink, which vocabulary — and the tests assert that
it supplies nothing else.
What is deliberately absent¶
- No driver imports outside
graph.pyanddsn.py— a route that reachedasyncpgorneo4jwould be a path around the repository and the tenant binding. - No second error shape — routes do not catch typed errors; the envelope answers them.
- No tenant parameter anywhere — the tenant comes from the credential, and the tests assert that a header claiming one changes nothing.
- No caching on the request path — the seam is documented, and the example does not use it: a cache is a second copy of the truth, and this example's point is the first one.