> ## Documentation Index
> Fetch the complete documentation index at: https://voyant.travel/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Architecture

> The execution model behind a deployed Voyant app: the resident Node runtime, the request lifecycle, how the deployment graph is composed, and one database plus one runtime per organization.

This page explains how a Voyant application actually runs. It goes deeper than the [architecture overview](/docs/concepts/architecture): where the code lives, how a request travels through it, how a deployment is assembled at build time, and why tenancy is a deployment fact rather than a runtime check. Read it once and the rest of the fundamentals will click into place.

## The layered boundary

Voyant keeps one hard separation, and almost everything else follows from it.

* **Packages** (`@voyant-travel/*`) hold reusable business logic: schemas, domain services, route surfaces, contracts, and adapters. They are framework-agnostic. A package does not know how or where it is hosted, and it does not own deployment shape.
* **App shells** (generated projects, and your forked or generated deployment) own the things a package cannot: UI, auth wiring, the deployment manifest, runtime configuration, secrets, and the migration history.
* **Transport adapters** stay thin. A Hono route handler validates input, calls a shared domain service, and shapes the response. It does not hold business logic. The same service can be driven by a different transport without rewriting the domain.

<Note>
  This boundary is why the same `@voyant-travel/bookings` logic runs unchanged whether you self-host the container or use Voyant. The domain code never learned where it lives.
</Note>

The three concerns map to three different lifecycles too. Packages version independently and you consume them as ordinary dependencies. The app shell is yours to own and upgrade. Transport adapters are the seam where the two meet, so they are deliberately boring.

## The runtime

Voyant runs as a **resident Node process against Postgres**. The deployable is a
container image, and it is the same artifact however it is hosted.

| Layer         | Technology                                                                   |
| ------------- | ---------------------------------------------------------------------------- |
| Runtime       | Node, long-lived resident process                                            |
| Database      | PostgreSQL, accessed through Drizzle ORM                                     |
| API transport | Hono, with optional platform route helpers                                   |
| Frontend      | TanStack Start and React 19                                                  |
| Auth          | Better Auth in generated projects; core packages stay auth-provider agnostic |

The server listens on `PORT` (default `8080`) and exposes `/healthz` for
container probes.

Node is the target for the composed application. Edge-native storefront and
federated surfaces keep their own hosts and are not part of the composed graph.

## The request lifecycle

A request to a deployed Voyant app travels through a predictable set of layers.

<Steps>
  <Step title="Process start (once)">
    The resolved graph boots: modules register, provider bindings resolve, and subscribers and jobs are activated. This happens once at process start, not per request.
  </Step>

  <Step title="Entrypoint routing">
    The HTTP server inspects the URL and separates SSR HTML, `/api/*`, and the other branches.
  </Step>

  <Step title="Transport (Hono)">
    For `/api/*`, the Hono app receives the request. Middleware runs, the route matches, and the handler parses and validates input against the module's contract.
  </Step>

  <Step title="Domain service">
    The route calls a shared domain service from the owning module. This is where business rules live. The service talks to its own tables through Drizzle.
  </Step>

  <Step title="Database">
    Drizzle issues SQL against the single Postgres database for this deployment. Cross-module reads stitch through links and fetchers (see [Links](/docs/concepts/how-it-is-built)), not through cross-module foreign keys.
  </Step>

  <Step title="Response shaping">
    The service result is shaped to the wire contract and returned. The `-contracts` package keeps that shape identical on both ends, so the `-react` client deserializes it with no guesswork.
  </Step>
</Steps>

Two execution shapes sit alongside the request path and are deliberately kept out of it:

* **Jobs** are package-owned scheduled or wakeable background behavior. The host supplies delivery, leases, retries, and health; domain records remain authoritative. See [Jobs](/docs/platform/jobs).
* **Schedules** are one way a package declares when its job should run. They do not contain business logic.

Voyant owns the job contract but not every hosting implementation. Voyant and self-hosters can supply different scheduling and lease adapters without changing package behavior.

## How a deployed app is composed

A Voyant deployment is not a runtime plugin loader. It is a **build-time composition**: a versioned, declarative package graph selected at build time and lowered to one resident Node application.

The deployment declares only its *differences* from the standard product in [`voyant.config.ts`](/docs/platform/fundamentals/configuration): the target and mode, its infrastructure provider bindings, and any distribution bundles it installs. The build resolves that against the versioned product distribution into a complete graph, from which the schema set, routes, and admin surface are derived. Provider choices are **bound at boot** inside the fixed graph, never baked into the platform.

The direction the platform is moving captures the intent precisely: a standard deployment shrinks to its identity.

```ts theme={null}
// The shape of a thin deployment: the framework-owned module set + the
// providers it chose + its own 20%. `createVoyantApp` (from
// @voyant-travel/framework) assembles the standard manifest and registry.
import { createVoyantApp } from "@voyant-travel/framework"

export const app = createVoyantApp({
  providers: buildProviders(), // card, storage, flights connector, …
  modules: deploymentLocalModules,        // the deployment's own modules
  extensions: deploymentLocalExtensions,  // deployment-local customizations
  plugins,
  auth,
})
```

The split is the whole point:

* **Config-derived wiring** (which modules, mount order, surfaces) belongs to the platform, derived from `config.modules`.
* **Provider and deployment wiring** (db, env, KMS, the injected card-payment starter, connectors) stays in the deployment and is passed in.

Custom work layers through first-class seams (`src/extensions`, `src/admin`, `src/subscribers`) that are auto-discovered at build time. None of it edits a platform-owned file, so it survives upgrades. See [How Voyant is built](/docs/concepts/how-it-is-built).

```mermaid theme={null}
flowchart TD
    cfg["voyant.config.ts (manifest)"] --> asm["Framework assembly (build time)"]
    asm --> schemas["Derived schema set + migrations"]
    asm --> routes["Mounted module routes"]
    asm --> admin["Generated admin chrome"]
    prov["Bound providers: db, payments, storage, connectors"] --> app["Deployed application"]
    custom["src/modules · src/extensions · src/links · src/admin"] --> app
    schemas --> app
    routes --> app
    admin --> app
```

## One database, one runtime per organization

Voyant's tenancy model is deliberately simple and is the most important architectural decision in the platform.

> **One Postgres database plus one runtime per organization.** Tenancy is enforced at the deployment boundary, not by in-process middleware.

This is [ADR-0001](https://github.com/voyant-travel/voyant/blob/main/docs/adr/0001-tenant-scoping.md), and it is an explicit, accepted decision rather than an accident.

### What it means concretely

The platform ships **no** in-process organization-scoping. There is no `requireOrgId` middleware in `@voyant-travel/hono`, no org-scoped wrapper around the Drizzle client, and no query interceptor that auto-applies an `organizationId` filter. When you ask "how is one customer's data isolated from another's?", the answer is **separate database, separate compute runtime**.

Provisioning is the enforcement: one database and one runtime per customer organization, and provisioning must refuse to point two organizations at the same database. Self-hosters inherit the identical topology and own that invariant themselves.

<Note>
  A column named `organizationId` is still fine as **data**: "which organization owns this booking" within one deployment is metadata, not isolation. The forbidden use is threading `organizationId` through a query to **partition data across customers**. That is the deployment boundary's job, and there is no cross-tenant data in the database to partition in the first place.
</Note>

### Why this model

The trade-off was weighed and chosen for clear reasons.

* **No per-query overhead.** Every read stays one round-trip with no injected predicate.
* **No confused-deputy bugs.** There is no shared schema where a forgotten filter could leak across tenants, because there is no cross-tenant data in the same database.
* **Hard to bypass accidentally.** A misconfigured runtime cannot read another customer's database unless someone hard-codes the wrong connection string.
* **Module authors carry no tenancy concern.** A package author writes a list query and ships it; isolation is provisioning's problem.

The honest costs were accepted too: there is no defense in depth, self-hosters must understand they own the isolation guarantee, and a future shared-tier would require revisiting this ADR before any such work starts. Adding in-process scoping later is feasible; ripping it out of every read path if it bloated performance would be harder, so the platform defaults to the real product reality of single-tenant deployments.

## Self-host versus Voyant

The platform is the same in both; only ownership of the runtime differs.

<CardGroup cols={2}>
  <Card title="Voyant OSS">
    You run the published container image on infrastructure you own, and you supply the Postgres database, the secrets, and the deployment lifecycle. You run the same one-database-per-organization topology and are responsible for the isolation invariant.
  </Card>

  <Card title="Voyant">
    The database and runtime are provisioned, operated, upgraded, and scaled for you, and the no-shared-database invariant is enforced in provisioning. The packages and your config are identical to the self-host case.
  </Card>
</CardGroup>

Because the boundary is the same, moving between the two is a deployment-and-provisioning concern, not a rewrite. The app shell, the config manifest, and the packages do not change.

## Next steps

<CardGroup cols={2}>
  <Card title="Modules" icon="cubes" href="/docs/concepts/how-it-is-built">
    The anatomy of a module: what it owns and how modules compose.
  </Card>

  <Card title="Configuration" icon="sliders" href="/docs/platform/fundamentals/configuration">
    How `voyant.config.ts` drives graph resolution.
  </Card>

  <Card title="Data models" icon="table" href="/docs/concepts/how-it-is-built">
    Schema authoring, money, indexes, and migrations.
  </Card>

  <Card title="Jobs" icon="clock" href="/docs/platform/jobs">
    Package-owned background work off the request path.
  </Card>
</CardGroup>
