Deploy the ASGI application
Tenchi produces an ordinary ASGI application. Production readiness comes from explicit lifecycle ownership, middleware, health checks, observability, and a repeatable contract gate.
Start with the production handbook when the application still needs configuration, transaction, retry, worker, or telemetry decisions. This page covers the final process and release boundary.
Run an ASGI server
The generated application exposes app.server.asgi:app:
uv run uvicorn app.server.asgi:app \
--host 0.0.0.0 \
--port 8000 \
--proxy-headersUse your platform's process manager, worker count, graceful shutdown, and
request-timeout guidance. tenchi dev enables reload and is not the production
entrypoint.
Own resources with lifespan
Open database pools, SDK clients, and other process-scoped resources in the application lifespan. Yield a small state object and build each request context from it. Cleanup runs during graceful shutdown.
Use a request-scoped context manager for transactions or ports whose success
depends on the request outcome. Application exceptions pass through its
__aexit__ before Tenchi maps them to HTTP, allowing commit-on-success and
rollback-on-error behavior.
Load validated configuration before opening resources. Put database migrations in a controlled release step; use lifespan to open pools and verify the schema generation that the process expects.
Add ASGI middleware directly
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
middleware = [
Middleware(
TrustedHostMiddleware,
allowed_hosts=["api.example.com"],
),
Middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_methods=["*"],
allow_headers=["authorization", "content-type"],
),
]
app = create_app(
routes=routes,
context_factory=create_context,
lifespan=lifespan,
middleware=middleware,
)Configure HTTPS and forwarded headers at the trusted reverse proxy boundary. Keep host and origin allowlists explicit.
The generated Swagger UI loads pinned CDN assets with subresource integrity.
For an offline deployment, self-host the assets and pass their URLs to
swagger_ui_route(). A content-security policy must also allow the page's
fixed inline initializer and style, normally through their hashes. Protect the
UI and openapi_route() together when the API description is not public.
Separate liveness from readiness
Use different routes for the two deployment decisions. Liveness answers whether the process is running and responsive, so it must not depend on a database, cache, model provider, or another network service. Readiness answers whether this process should receive traffic and may check dependencies required by the operations it serves:
from tenchi.health import health_route
liveness = health_route(path="/live")
readiness = health_route(
path="/ready",
checks={"database": check_database},
check_timeout=2.0,
)Register both routes at the composition root. Configure the platform's
liveness probe to call /live and its readiness probe to call /ready. A
database outage can then remove this process from traffic without causing a
restart loop.
Readiness checks run concurrently and return 503 when any check is unhealthy
or exceeds check_timeout. Synchronous checks run in worker threads so
blocking I/O cannot hold the event loop past the health deadline; the
underlying thread may finish after the timed-out response, so prefer async
clients with their own deadlines. An async check that suppresses cancellation
may also finish in the background, but it cannot delay the response. Keep
checks read-only and independently bounded. Health routes are public by
default; pass public=False when infrastructure can authenticate probes.
Limits and deadlines
The server limits request bodies to 1 MiB by default. Set
max_request_bytes= globally or on an individual contract. A contract's
timeout= bounds only its HTTP route. Put an entrypoint-neutral
deadline around application work
that needs the same bound through tools, jobs, tasks, and direct execution.
Also configure transport and platform timeouts.
Observe outcomes
RequestInfo includes a trusted request ID. Tenchi accepts a valid inbound
x-request-id or creates one and includes it on responses. Outcome observers
receive the request, final status, duration, and application/framework error
source after request-scoped cleanup.
Use request and use-case observers or ASGI middleware to feed structured logs, metrics, and traces. They should never contain request bodies, credentials, or error details unless the application deliberately redacts them.
See Observability and audit for label, correlation, worker, and redaction conventions.
Release sequence
Run the local application gate and compare every snapshotted boundary with one baseline commit from the version currently deployed:
uv run tenchi verify --base-ref "$BASE_SHA"verify discovers the literal OpenAPI title, version, description, and
security configuration from app.server.routes:api_routes. It runs the same
deterministic checks, rejects an application map with diagnostics or unresolved
relationships, and compares
OpenAPI, durable job messages, application tools, and evaluation policy with
the resolved commit. It also compares tenchi.toml with that commit and keeps
the stronger current-or-deployed requirement active, so a release cannot avoid
a gate by weakening it in the same change. Retain the complete
receipt before making another edit: its source commit and tree digest identify the exact
application state that passed, and verification fails if that state changes
while the command is running.
Use its metadata flags only when the application does not keep those values as
literal route-module declarations.
Missing historical job or evaluation snapshots fail by default. If that commit
predates only jobs.json, add --allow-missing-job-baseline once and confirm
the result records a job manifest baseline metadata change. Use
--allow-missing-evaluation-baseline in the same way for evaluations.json.
Remove either override after its baseline lands.
Then release in this order:
- Verify production settings and secret references without printing their values.
- Back up data according to the datastore's recovery plan.
- Run backward-compatible database migrations once.
- Start the new API generation without sending it traffic.
- Run
tenchi preflightagainst the target environment. - For model-backed behavior, run the explicitly budgeted
tenchi eval rungate with the provider configuration this release will use. - Start or update background workers that can consume messages from both application generations. Deploy compatible consumers before new producers receive traffic.
- Wait for startup and readiness checks, then shift traffic gradually.
- Smoke-test a public route and an authenticated operation through the production edge.
- Watch error rate, latency, database saturation, and queue age during the rollout.
tenchi check does not contact production dependencies, validate secret
manager permissions, run migrations, or prove that an external worker is
healthy. Keep migrations as their own release step, then use
tenchi preflight for read-only, timeout-bounded dependency
checks where the target environment is available. AI evaluation execution
remains separate because it may be nondeterministic and incur provider cost;
the declared policy is still checked locally.
Plan rollback before rollout. Application rollback must remain compatible with the migrated schema and any work records already written by the new generation.
Raw httpx.ASGITransport does not run lifespan by itself. Prefer
tenchi.testing.open_client() or open_http(), which start and stop the
application correctly.