App architecture
Tenchi applications organize code by feature and make infrastructure and server composition explicit.
app/
features/<feature>/
contracts.py
schemas.py
ports.py
policy.py
routes.py
jobs.py
tasks.py
tools.py
use_cases/
tests/
shared/
infra/
server/
context.py
hooks.py
webhooks.py
routes.py
jobs.py
runtime.py
preflight.py
tasks.py
tools.py
mcp.py
asgi.py
tests/Feature boundary
contracts.py owns HTTP declarations. schemas.py owns Pydantic models shared
by contracts, ports, and use cases. ports.py defines the interfaces the
feature needs. policy.py owns pure authorization rules. routes.py binds
contracts to use cases. tasks.py gives selected use cases stable operational
names. jobs.py declares durable background messages without importing their
consumer use cases. tools.py gives selected use cases stable, typed
machine-facing contracts.
Each file in use_cases/ contains one plain async function. Unit tests sit
beside the feature and call those functions directly with memory adapters.
Shared kernel
app/shared/ contains application-wide errors and concepts shared by several
features, such as authenticated users. Shared code must not depend on a
feature; otherwise the dependency direction points both ways.
Infrastructure
app/infra/ implements feature-owned ports and exposes explicit wiring
functions. Infrastructure may depend on database drivers or external SDKs, but
never on use cases, routes, contracts, or server composition.
Server composition
The app/server/ package is the application root:
context.pydeclares the frozenAppContext.hooks.pyimplements HTTP-boundary concerns such as authentication.webhooks.pybinds signed-provider verification and service identity.routes.pycombines feature route groups and shared error declarations.jobs.pybinds feature job declarations to consumer use cases.runtime.pyowns resources shared by HTTP and operational entrypoints.preflight.pydeclares read-only checks of the target deployment environment.tasks.pycomposes the application's operational task runner.tools.pycomposes application tools with authenticated context wiring.mcp.pyoptionally exposes those tools through authenticated MCP discovery and invocation.asgi.pycreates adapters, lifecycle resources, hooks, middleware, and the final Starlette application.
The composition root may import anything. Everything else follows a narrower direction.
Dependency direction
routes -> use cases -> ports -> schemas/domain
server composition -> routes
server composition -> infrastructure -> ports
server composition -> job declarations + consumer use cases
server composition -> tool bindings + authenticated contextArrows point from the importing layer to the layer it depends on. Server composition owns the complete graph; routes and infrastructure never depend on it.
- Schemas, domain code, and ports never import infrastructure or the HTTP runtime.
- Use cases may import schemas, ports, policies, context types, and shared application code.
- Routes may import contracts and use cases, but never infrastructure.
- Job declarations may import schemas but not use cases or infrastructure;
producers may import declarations and
job_message(). - Tool modules may import schemas and use cases to bind them, but never infrastructure or server composition. Safety annotations describe behavior; authorization remains in the use case.
- Infrastructure implements ports, but never imports use cases or routes.
- Server composition wires the complete graph.
Webhook verifiers live at server composition because they need secrets,
provider SDKs, and exact HTTP bytes. The verified service identity still enters
the use case through AppContext; provider payloads remain contract-owned
Pydantic input.
Run uv run tenchi map --feature <name> to inspect one feature and its direct
dependencies, uv run tenchi doctor to check these conventions alone, or
uv run tenchi check for the complete validation loop. Add --json to the map
when an agent or other tool will consume it.
A normal from app.server.context import AppContext import is the clearest
default. If an application needs to avoid that runtime import, enable
postponed annotations with from __future__ import annotations, then import
AppContext inside an if TYPE_CHECKING: block. Without postponed or quoted
annotations, Python raises NameError when it defines the use case. Route
binding ignores this app-owned context annotation while still checking every
contract-owned boundary annotation.
from __future__ import annotations
from typing import TYPE_CHECKING
from app.features.todos.schemas import CreateTodo, Todo
if TYPE_CHECKING:
from app.server.context import AppContext
async def create_todo(request: CreateTodo, context: AppContext) -> Todo:
return await context.todos.create(title=request.title)