Skip to content

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-headers

Use 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:

  1. Verify production settings and secret references without printing their values.
  2. Back up data according to the datastore's recovery plan.
  3. Run backward-compatible database migrations once.
  4. Start the new API generation without sending it traffic.
  5. Run tenchi preflight against the target environment.
  6. For model-backed behavior, run the explicitly budgeted tenchi eval run gate with the provider configuration this release will use.
  7. Start or update background workers that can consume messages from both application generations. Deploy compatible consumers before new producers receive traffic.
  8. Wait for startup and readiness checks, then shift traffic gradually.
  9. Smoke-test a public route and an authenticated operation through the production edge.
  10. 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.

Exercise lifespan in tests

Raw httpx.ASGITransport does not run lifespan by itself. Prefer tenchi.testing.open_client() or open_http(), which start and stop the application correctly.