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 validated correlation 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.

Callers can choose an accepted x-request-id; validation does not establish who supplied it. Configure your trusted proxy to replace it if you need edge-generated identifiers.

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"

Continue only when verification passes. Retain its report with the release.

Then release in this order:

  1. Verify production settings and secret references without printing their values.
  2. If the service stores data, back it up according to the datastore's recovery plan.
  3. If the release changes the database schema, run backward-compatible database migrations once.
  4. Start the new API generation without sending it traffic.
  5. If the application has preflight checks, 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. If the service enqueues work, 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 an application operation through the production edge, including authentication when the operation is protected.
  10. Watch error rate, latency, database saturation, and queue age during the rollout.

tenchi check runs your local validation commands; it does not establish production readiness. Those commands may contact services if your tests or imports do so. 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.

Understand verification failures

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.

When adding jobs, tools, or evaluations for the first time, verify handles a missing historical snapshot automatically if its default composition module was also absent at the baseline. If a job or evaluation module already existed but its first snapshot did not, use --allow-missing-job-baseline or --allow-missing-evaluation-baseline for that initial comparison. Confirm the corresponding job manifest baseline or evaluation manifest baseline metadata change. Do not use an override to bypass an existing snapshot at a renamed or mistyped path. The CLI reference describes the complete policy.

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.