Skip to content

Mental model

Tenchi has one architecture: typed contracts at the boundary, plain use cases at the center, and explicit dependency wiring around them.

Contract

A frozen declaration of one HTTP operation: method, path, validated inputs, successful responses, expected application errors, media types, metadata, and runtime limits. A contract contains no handler logic.

Route

A binding between a contract and a use case. route() inspects the use-case signature immediately, so mismatched parameter names or annotations fail while the application is composed rather than on the first request.

Use case

A plain async function that implements one application action. Its parameters are named boundary values such as request, params, query, headers, and context. It does not receive a Starlette request.

Operational task

A stable name bound to a use case for a backfill, repair, replay, or maintenance command. A task validates its input and result and runs with application lifecycle resources. It is an invocation boundary, not a scheduler or queue.

Background job

A durable, stable name with validated request and result types. Producers serialize a JobMessage; the composition root binds the declaration to a plain async consumer; a dispatcher validates delivery. Queue persistence, retries, acknowledgement, and scheduling stay in infrastructure.

Application tool

A stable machine-facing name with validated input, output, declared errors, and safety annotations, explicitly bound to a plain async use case. A tool runner supplies application lifecycle and context wiring. Model providers, agent loops, authentication, approval interfaces, and transport protocols stay outside the declaration.

Evaluation

A named suite of typed cases, normalized metrics, passing thresholds, and optional token and cost budgets for AI-powered behavior. An evaluation runner supplies application lifecycle and a scoped context to each case. Models, prompts, judges, and datasets stay application-owned; reports contain scores and usage, not evaluated payloads.

Port

An interface the application needs, declared with typing.Protocol. The feature owns the port; infrastructure supplies an adapter. This keeps the application independent of a database driver, HTTP SDK, cache, or queue.

Context

A frozen dataclass carrying the dependencies and verified identity available to a use case. The application constructs it explicitly. Tenchi does not use a service locator or dependency-injection container.

Hook

An HTTP-boundary function that sees RequestInfo and the current context. A hook can authenticate a request and return an enriched context. Hooks are for boundary concerns, not business authorization.

Policy

A pure application function that answers an authorization question about a known subject. The use case loads the subject through a port and asks the policy. Because policies perform no I/O, they are easy to test and reusable from HTTP, workers, scripts, and tests.

Adapter

A concrete implementation of a port. SQL repositories, external API clients, and memory test doubles are adapters. They live under app/infra/ and are wired at the server composition root.

Boundary flow

HTTP request
  -> route match
  -> request id and context factory / request scope
  -> boundary hooks
  -> size, media-type, and Pydantic input validation
  -> plain use case
  -> response presenter and header projector
  -> Pydantic response validation
  -> HTTP response

The typed client runs the complementary checks: it serializes inputs from the same contract and validates the status, media type, body, headers, and declared application errors returned by a server.

The key tradeoff

Tenchi asks for more structure than a one-file microframework example. In return, an endpoint's validation, client behavior, OpenAPI, compatibility policy, and application boundary do not become separate models as the service grows.