Skip to content

11. Backend

Context and decision

The Vessel is one explicitly composed Litestar application. Its HTTP, persistence, extension, and worker collaborators are assembled once, published only when whole, and retired in reverse dependency order.

Decision

src/lychd/app.py:create_app() is the Vessel composition root. It creates one Litestar application with AppInit; imports and route declarations do not mutate a pre-existing app. Core controllers and the compiled Altar fallback are collected there. Extensions are selected explicitly, receive a host-created context, and contribute shaped records rather than arbitrary route or middleware mutation. There is no delivered extension route bundle.

The native Click CLI remains useful without an application graph. serve and database delegate lazily to Litestar; help, init, bind, lifecycle commands, and ordinary operator commands do not construct the ASGI application.

The runtime is one Granian/Litestar process and one event loop. The native launcher rejects multi-worker and reload configuration. The launcher and create_app() call one pure server policy which rejects server-visible worker/reload variables and detectable direct Litestar/Granian arguments. Listener authority resolves explicit --port, then LITESTAR_PORT, GRANIAN_PORT, and the configured server port; the native launcher publishes that result before Litestar loads the application, and Host admission consumes the same value. Native lychd serve likewise accepts only 127.0.0.1 or ::1 from --host/-H, LITESTAR_HOST, or GRANIAN_HOST, defaulting to 127.0.0.1, and publishes that host before delegation. File-descriptor and UNIX-domain-socket arguments or Litestar environment overrides are refused: otherwise they would bypass the owned TCP host rather than refine it. The caged image's intentional internal Granian 0.0.0.0 listener is a distinct topology behind generated loopback-only host publication; it does not authorize the native bootstrap listener to widen. The run-event bus, cancellation coordinator, services, and SAQ workers are process-local. This is a correctness boundary, not a scalability claim or permission to use another launcher.

Lifespan and ownership

Application initialization creates the typed Settings generation, selected extension assembly, and validated Rune registry; installs routes, middleware, framework plugins, and dependency providers; then enters its lifespan. Lifespan waits for the Host Reactor fence, connects both queues, builds durable dependencies and services, loads runtime registry material off the event loop, publishes the shared run substrate, and attempts preauthorization and orphan/consent reconciliation. app.state.services is published only after required construction succeeds. Reconciliation may log a failure and continue; required setup cannot.

Any failed startup follows the same reverse order as shutdown: stop in-process workers, withdraw the run substrate, close services, then disconnect queues. A collaborator therefore cannot outlive what it reads. There is no hot-reload contract.

The application layer owns HTTP admission and dependency wiring. Domain code speaks ports, repositories, and services; persistence adapters own SQLAlchemy sessions and explicit statements needed for locking or aggregate transitions. Controllers never issue database queries directly. The Nexus controller uses a profile-bound request-admission port before it creates a physical transition task; PostgreSQL owns the cross-process uniqueness transaction while the controller retains only short-lived ticket projection. The system layer owns host adapters and lifecycle effects. Persistence (06), Configuration (12), and Security (09) retain their respective transaction, configuration, and trust laws.

Interfaces rather than rows

Public endpoints use versioned Pydantic or dataclass DTOs and named operations. An explicitly bounded SQLAlchemyDTO is permitted for a true CRUD projection, but ORM shape never becomes the public API by accident. Includes, exclusions, aliases, nested depth, input rules, SSE envelopes, and compatibility are interface contracts with tests.

Litestar owns typed HTTP/OpenAPI and JSON serialization. Runtime and offline export share one JSON-only OpenAPI configuration. Controller validation and mapped application/repository failures retain Litestar's declared JSON shapes rather than an undeclared Problem Details transform; boundary middleware may still return its own non-API rejection. The static server delivers compiled Svelte assets, not browser state or templates. Mermaid is sent as inert source for client-side rendering. Structlog is present instrumentation; an uninstalled OpenTelemetry exporter does not establish external tracing.

The process owns one async SQLAlchemy engine/session factory. Connection and signing secrets are resolved only by their consuming component; Settings retain references, never secret contents. The asyncpg hook registers separate binary codecs: JSONB adds and removes PostgreSQL's version byte, while plain json passes its unversioned bytes directly. Focused hook tests pin both wire shapes; this is a correctness contract, not a throughput claim.

Trust boundary and evidence

The Vessel is the trusted control plane: HTTP admission, orchestration, persistence access, runtime projection, and its static client. Its default browser seam admits literal loopback Host authorities on the configured external port and detected listener port, same-origin requests, and only explicitly configured loopback CORS origins. This does not authenticate the fixed bootstrap Sigil. Wider browser controls and queue/execution isolation belong to Security (09). Tomb is a designed, not delivered, execution plane; there is no Tomb queue, executor, credential, mount, sandbox, or promotion authority here. Its delivery boundary is maintained by State of Work.

Focused tests cover memory-profile composition and web contracts. A disposable two-boot receipt exercises the real application factory, PostgreSQL, in-process SAQ, and asyncpg installation with an offline model and HTTP test client; a focused disposable receipt round-trips both plain json and JSONB through the same engine factory. Neither proves a real browser, inference engine, or deployed host. State of Work owns the evidence envelope.

Consequences

  • Framework upgrades must reprove initialization, dependency injection, serialization, OpenAPI, and ordered shutdown.
  • Current streaming and cancellation semantics require one process. A multi-process Vessel needs durable event and ownership protocols, not a changed worker count.
  • Explicit DTOs may deliberately duplicate stable storage facts; migrations do not thereby set the HTTP compatibility contract.