Skip to main content
This page explains how a Voyant application actually runs. It goes deeper than the architecture overview: 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.
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.
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. 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.
1

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.
2

Entrypoint routing

The HTTP server inspects the URL and separates SSR HTML, /api/*, and the other branches.
3

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.
4

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.
5

Database

Drizzle issues SQL against the single Postgres database for this deployment. Cross-module reads stitch through links and fetchers (see Links), not through cross-module foreign keys.
6

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.
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.
  • 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: 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.
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.

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, 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.
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.

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.

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.

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

Modules

The anatomy of a module: what it owns and how modules compose.

Configuration

How voyant.config.ts drives graph resolution.

Data models

Schema authoring, money, indexes, and migrations.

Jobs

Package-owned background work off the request path.