Skip to content

Verify a deployment environment

tenchi preflight answers a different question from tenchi check:

CommandQuestionExpected environment
tenchi checkIs this source tree internally valid?Deterministic and local
tenchi preflightCan this release safely use the environment it is about to enter?The target deployment environment
Health and readiness routesCan this running process serve traffic now?A live application process
tenchi task runShould an authorized operator change application state?An explicitly selected operational environment

Use preflight after configuration and migrations are available but before the new application receives traffic. Typical checks cover database connectivity, the deployed schema version, secret-manager permissions, required outbound services, and worker heartbeats.

Declare read-only observations

For the generated application's SQLite database, first add a connection factory that opens an existing database in read-only mode:

# app/infra/preflight_database.py
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path

import aiosqlite


@asynccontextmanager
async def open_preflight_connection(
    database_path: str,
) -> AsyncIterator[aiosqlite.Connection]:
    uri = f"{Path(database_path).resolve().as_uri()}?mode=ro"
    async with aiosqlite.connect(uri, uri=True) as connection:
        yield connection

Create app/server/preflight.py exposing checks, the module tenchi preflight loads by default (tenchi new --full generates an empty one). Each observation is a zero-argument async function that returns None on success and raises on failure. This example assumes your migrations set SQLite's user_version to 7; replace that value with the version your release requires. The starter does not set a migration version for you.

# app/server/preflight.py
from app.infra.preflight_database import open_preflight_connection
from app.server.runtime import DATABASE_PATH
from tenchi.preflight import preflight_check, preflight_group


async def database_connectivity() -> None:
    async with open_preflight_connection(DATABASE_PATH) as connection:
        await connection.execute("SELECT 1")


async def database_schema() -> None:
    async with open_preflight_connection(DATABASE_PATH) as connection:
        cursor = await connection.execute("PRAGMA user_version")
        row = await cursor.fetchone()
        if row is None or int(row[0]) != 7:
            raise RuntimeError("unexpected database schema")


checks = preflight_group(
    preflight_check(
        "database.connectivity",
        database_connectivity,
        description="Open the configured database in read-only mode.",
        failure_code="DATABASE_UNAVAILABLE",
    ),
    preflight_check(
        "database.schema",
        database_schema,
        description="Verify the schema required by this release.",
        failure_code="DATABASE_SCHEMA_MISMATCH",
    ),
)

Checks default to a five-second timeout. Set a shorter or longer declaration timeout only when the dependency has a known response budget:

preflight_check(
    "workers.email",
    email_worker_heartbeat,
    timeout=2.0,
    failure_code="EMAIL_WORKER_NOT_READY",
)

Tenchi runs the checks concurrently and reports results in declaration order. Naming and shape rules are listed under Constraints.

Keep the boundary read-only

Preflight does not receive an application context and does not start the application lifespan. Each check must open the narrowest read-only client it needs. For a database, use a read-only connection or account. For a secret manager, request metadata or describe a known secret instead of returning its value. For a worker, read its heartbeat rather than enqueueing probe work.

Read-only is an application guarantee

Python cannot prove that an arbitrary async function has no side effects. Tenchi enforces the narrow function shape, supplies no stateful context, marks the MCP tool read-only, and keeps operational mutations in tenchi task run. Your adapter permissions are the final enforcement boundary. Use credentials that cannot write.

Do not migrate schemas, repair records, rotate secrets, enqueue jobs, or call business endpoints from preflight. Put migrations in the release process and authorized repairs or backfills in operational tasks.

Check the production concerns

Keep each dependency independently named so a failed rollout points to one owner:

Preflight should prove only what must be true before traffic shifts. Leave ongoing dependency monitoring to health checks and observability.

Run the deployment gate

From the application root:

uv run tenchi preflight
uv run tenchi preflight --json
uv run tenchi preflight --timeout 3

--timeout caps every declared timeout; it can make the gate stricter but never extend a check's own limit. The command exits zero only when every check passes. Human output names each status and stable failure code. JSON returns the same versioned result model as the MCP tool:

{
  "schema_version": 12,
  "root": "/srv/api",
  "target": "app.server.preflight:checks",
  "ok": false,
  "counts": {
    "passed": 1,
    "failed": 1,
    "timed_out": 0,
    "total": 2
  },
  "duration_seconds": 0.031,
  "checks": [
    {
      "name": "database.connectivity",
      "description": "Open the configured database in read-only mode.",
      "status": "passed",
      "duration_seconds": 0.012,
      "failure_code": null
    },
    {
      "name": "database.schema",
      "description": "Verify the schema required by this release.",
      "status": "failed",
      "duration_seconds": 0.03,
      "failure_code": "DATABASE_SCHEMA_MISMATCH"
    }
  ]
}

Exception types, exception messages, and return values never appear in the result.

Configure application logging separately

The CLI and MCP tool discard direct stdout and stderr from preflight code. They cannot intercept handlers that send logs directly to files, sockets, or telemetry backends. Configure dependency logging for production and never include secret values in application log records.

Use a different module only when the application cannot follow the generated convention:

uv run tenchi preflight --preflight my_app.release:checks

For MCP, pass the same target when the server starts:

uv run tenchi mcp --preflight my_app.release:checks

The MCP preflight tool is available without enabling task execution. It uses the credentials and environment captured by the MCP server process, so call it only when that process is connected to the intended deployment target.

Place it in the rollout

A safe release sequence is:

  1. Validate the source and contracts with tenchi check and the OpenAPI, job-message, application-tool, and evaluation-policy compatibility gates.
  2. Load production configuration without printing secret values.
  3. Run backward-compatible migrations once.
  4. Start the new generation without traffic.
  5. Run tenchi preflight from that generation's target environment.
  6. Wait for live readiness, then shift traffic gradually.
  7. Watch request and worker telemetry throughout the rollout.

Continue with deployment for health routes, server limits, rollout, and rollback guidance.

Constraints