Running in production

Deploying

Build and run an Aventara server in production - environment variables, the build, PostgreSQL, schema changes with db push or migrate, CORS, and generating and committing the frontend client.

Read as Markdown

An Aventara server made by aventara new is a NestJS application. You deploy it like any Node service: install, build, run dist/main. A Next.js app that hosts Aventara deploys as a Next.js app: see Next.js. The framework adds three things to think about: the generated files (the discovery artifact and the rest of what aventara:prepare writes) that must exist before the build, the database schema that must exist before the first request, and the frontend client, which is bound to the contract your server serves.

Environment

The scaffold reads two variables:

VariableMeaningDefault
DATABASE_URLThe database connection string. The server will not start without it: DATABASE_URL is not set: put it in .env (the start scripts load it) or in the environment. The check runs while Nest builds the application, before it listens.none
PORTThe port main.ts listens on.3000

Locally they live in .env. In production, set them in the platform's environment. npm run start:prod is:

Code
"start:prod": "node --env-file-if-exists=.env dist/main"

--env-file-if-exists loads .env when it is present and does nothing when it is not, so the same script works on a laptop and on a platform with no file. A real environment variable wins over .env: with DATABASE_URL=file:./override.db npm run start:prod, the server used override.db, not the .env value.

Do not commit .env. The scaffold's .gitignore already lists it, with /src/generated/ (or /src/aventara/, in a project whose Prisma client is generated into src/generated) and the SQLite files.

Build and run

Code
npm ci                 # installs, then runs `aventara:prepare` (the postinstall script)
npm run build          # nest build -> dist/
npm run start:prod     # node --env-file-if-exists=.env dist/main

npm ci runs the project's postinstall script, which runs aventara:prepare: it reads prisma/schema.prisma, runs prisma generate, writes src/generated/aventara/discovery.artifact.ts, then runs aventara-build, which writes resources.generated.ts, computed-fields.generated.ts and contracts.generated.ts beside it. All are generated and git-ignored, and nest build needs them. If your pipeline installs with scripts disabled (npm ci --ignore-scripts), run npm run aventara:prepare yourself before npm run build. Re-run it whenever the schema changes.

Prefer building once and running the same artifact everywhere: build in CI, copy dist/ and a production node_modules, and give each environment its own DATABASE_URL. Your Prisma client is generated for the provider in schema.prisma (SQLite or PostgreSQL), so build with the same schema you deploy.

On start, the host logs one line you can use as a readiness check:

Code
Aventara mounted 45 operations at /api (+ GET /api/_contract, POST /api/_transactions, POST /api/_all)

Call app.enableShutdownHooks() in main.ts so Nest closes the Prisma client on SIGTERM.

Build-time Contracts

A long-running Nest server compiles its Contracts when it starts, which takes tens of milliseconds, and needs nothing more. On serverless and edge platforms every cold start pays that. aventara-build compiles the Contracts once, at build time, and writes them to contracts.generated.ts. Pass that file as prebuilt where you create the framework:

Code
// a serverless or edge entry
import { createFramework } from "@aventara/core";
import { createPrismaAdapter } from "@aventara/prisma7-adapter";
import adapter from "./aventara.adapter.js";
import { aventaraConfig } from "./aventara.config.js";
import { aventaraContracts } from "./generated/aventara/contracts.generated.js";
import { PrismaService } from "./prisma.service.js";

const prisma = new PrismaService();

export const framework = await createFramework({
  ...aventaraConfig,
  adapter: await createPrismaAdapter({ ...adapter, client: prisma }),
  prebuilt: aventaraContracts,
});

PrismaService is the file the scaffold writes (src/prisma.service.ts): a PrismaClient subclass that reads DATABASE_URL. Outside Nest, build it directly with new PrismaService(), as above. The Contracts, their hashes and GET /api/_contract are the same as when compiled at startup. The scaffolded Nest server passes no prebuilt.

The framework uses the file only when this @aventara/core compiled it, for this protocol version, from the same configuration and the same database model. Otherwise it ignores the whole file, compiles at startup as if there were none, and prints one warning, AVENTARA_PREBUILT_STALE, that says what changed (the Resources whose configuration or model changed, or the version that differs). A stale file never serves a wrong Contract. It only costs the startup time it was meant to save.

Add a check to CI so a stale file fails the pipeline before the deploy:

Code
npx aventara-build --check

--check writes nothing. It exits 1 when resources.generated.ts, computed-fields.generated.ts or contracts.generated.ts is missing or differs from what a build would write now, and says what changed, in the words of the AVENTARA_PREBUILT_STALE warning at startup:

Code
aventara-build: src/generated/aventara/contracts.generated.ts is stale: it was built by @aventara/core 1.0.0-rc.7, and this is 1.0.0-rc.8. Run aventara-build (npm run aventara:prepare) and deploy the file it writes.
aventara-build: src/generated/aventara/contracts.generated.ts is stale: Resource User changed and the configuration or model outside the Resources changed since it was written. Run aventara-build (npm run aventara:prepare) and deploy the file it writes.

It names the core version that wrote the file against the installed one, a file format or protocol version that differs, the Resources that changed, are new or are gone, the configuration or model outside the Resources, or a file that no longer reads as a prebuilt Contracts file. A configuration that a server would refuse at startup (CONFIG_... or COMPILER_...) fails aventara-build itself, with the same message.

Two things follow from aventara-build running your configuration:

  • src/aventara.config.ts and everything it imports run at build time. Keep their top level free of side effects: no connection, no request.
  • A value read from process.env in the configuration is read when the build runs. If the deployed environment gives another value, the file reads as stale and the framework compiles at startup.

Where the three files go: beside the discovery artifact, in src/generated/aventara/, or in src/aventara/ in a project whose Prisma client is generated into src/generated (prisma generate empties that directory). See CLI reference.

The server starts without a database

The framework compiles its contracts from the discovery artifact, not from a live connection. A server whose database is unreachable still starts, still mounts its routes and still serves GET /api/_contract; a request that needs the database fails with A3001 ("Adapter execution failed."), and your diagnostics reporter gets the connection error. This is verified with DATABASE_URL="postgresql://u:p@127.0.0.1:1/db" (nothing listening). It is what lets CI generate the frontend client without a database (below). It also means a liveness probe on /api/_contract says nothing about the database: add your own health route for that, outside the protocol's _ paths (for example GET /api/health).

PostgreSQL

Create the project for PostgreSQL, or switch an existing one:

Code
npx @aventara/cli@rc new my-api --db postgresql

The scaffold then differs from SQLite in four places:

Code
datasource db {
  provider = "postgresql"
}
Code
// src/prisma.service.ts
import { PrismaPg } from "@prisma/adapter-pg";
super({ adapter: new PrismaPg({ connectionString: url }) });
Code
// src/aventara.adapter.ts
export default describePrismaAdapter({ discovery, provider: "postgresql", driver: "@prisma/adapter-pg" });
Code
"aventara:prepare": "aventara-prisma7-generate --schema prisma/schema.prisma --client src/generated/prisma --provider postgresql --driver @prisma/adapter-pg --out src/generated/aventara/discovery.artifact.ts --config src/aventara.config.ts && aventara-build"

and .env gets Prisma's placeholder, which you replace:

Code
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/DBNAME?schema=public"

provider and driver are declared, never sniffed: they must match your schema and the Prisma driver adapter you construct. The generate step checks --provider against your schema's datasource block and refuses a mismatch. It cannot check the driver. Changing database means changing all four, then npm run aventara:prepare.

Transaction options (timeout, maxWait, isolationLevel) go on the adapter (Configuration). SQLite supports Serializable only; on PostgreSQL pick the level your invariants need.

String filters (contains, startsWith, endsWith) follow the database by default: case-insensitive for ASCII on SQLite, case-sensitive on PostgreSQL. On PostgreSQL, pass mode: "sensitive" or mode: "insensitive" to make the behavior explicit; SQLite offers only "default" (Filtering).

Creating and changing the schema

Aventara never touches your database or your schema: that is Prisma's job. Two commands:

CommandUse it for
npx prisma db pushDevelopment and prototypes. Makes the database match schema.prisma right now. No migration history.
npx prisma migrate dev --name <change>Development, when you want a history. Writes prisma/migrations/<timestamp>_<change>/migration.sql and applies it. Commit the folder.
npx prisma migrate deployProduction. Applies the committed migrations that have not run yet. Never generates or resets.

For production, use migrations: migrate dev while developing, commit prisma/migrations/, and run migrate deploy as a release step before the new server version starts:

Code
DATABASE_URL=file:./mig.db npx prisma migrate dev --name init
# Applying migration `20261006225019_init`  ... Your database is now in sync with your schema.

DATABASE_URL=file:./mig2.db npx prisma migrate deploy
# 1 migration found ... All migrations have been successfully applied.

Prisma 7 reads the connection from prisma.config.ts, which the scaffold writes to load .env and read DATABASE_URL, so the commands see the same variable as the server.

After any schema change, in this order: change schema.prisma; migrate (or push); npm run aventara:prepare; rebuild; deploy. A new contract has a new hash, so regenerate the frontend client too.

The frontend client

The generated client is bound to the contract hash of the server it was generated from. A server whose contract changed answers a stale client with A2005 on every call. Follow these steps:

  1. Deploy (or start) the server.
  2. Run npm run avclient:generate in the frontend, with AVENTARA_API_URL pointing at it.
  3. Commit the generated files (src/api/AvClient.ts and src/api/generated/). They are deterministic: the same contract writes the same bytes, so a diff means the contract changed.
  4. Build the frontend.
Code
# in the frontend, in CI
AVENTARA_API_URL=https://staging.example.com/api npm run avclient:generate -- --yes
git diff --exit-code src/api         # fails the build if the committed client is stale

If the contract did not change, the generator prints avclient: up to date: ... nothing was written. and exits 0.

Generating in CI against a throwaway server

The server does not need a database to serve its contract, so a frontend pipeline can start the server itself:

Code
cd api
npm ci && npm run build
DATABASE_URL="file:./ci.db" PORT=3000 node dist/main &      # any valid URL will do; the contract needs no tables
npx wait-on http://localhost:3000/api/_contract
cd ../web
AVENTARA_API_URL=http://localhost:3000/api npm run avclient:generate -- --yes
git diff --exit-code src/api

Note: the commands above were each verified individually (build, start without a database, generate, up to date); the assembled CI script was not run end to end here.

Keeping versions together

The server's contract is checked on every call, so ship the server and the frontend that matches it. A server deployed with a new schema before the frontend is regenerated leaves old frontends getting A2005 until they update. Mitigations: deploy additive changes (a new nullable column does not break old clients' calls but still changes the hash, so the old client still gets A2005), release both together, or serve the previous frontend build until the new one is live.

CORS and browsers

A browser frontend on another origin needs CORS on the server. Nest's own enableCors works on Aventara routes: it is an HTTP-platform middleware, which also runs on the protocol's routes (NestJS host).

The aventara new scaffold enables it from CORS_ORIGINS, a comma-separated list in the environment. The scaffold's .env lists the local dev servers; in production, set it to your deployed frontend's origin (and remove the local ones if they should not be allowed):

Code
CORS_ORIGINS="https://app.example.com"

Unset or empty, CORS is off: no origin is allowed. (.env is not committed; set the variable in your hosting environment.) The protocol's custom headers must be allowed, and you may want the request id exposed to browser code. Both are enableCors options; the scaffold's call allows the headers by reflecting the request's, so add only the exposure:

Code
// src/main.ts
app.enableCors({
  origin: corsOrigins.length > 0 ? corsOrigins : false,
  exposedHeaders: ["Aventara-Request-Id"],
});

Verified: the preflight OPTIONS for content-type, aventara-protocol-version, aventara-contract-hash, aventara-request-id answers 204 with Access-Control-Allow-Origin: https://app.example.com, the requested headers allowed, and Access-Control-Expose-Headers: Aventara-Request-Id; the actual POST and GET /_contract carry the same origin and expose headers. Authentication via Authorization needs Authorization allowed too, which enableCors does by default by reflecting the request's headers.

CORS is not access control. It tells browsers which origins may read the answers; a server, a script or a bot ignores it. Guards decide who may call, and a rate limiter bounds how often.

Next.js

A Next.js app made with aventara new --host next (or init) deploys like any Next.js app on the Node.js runtime:

Code
npm ci                 # installs, then runs aventara:prepare (the postinstall script)
npm run build          # prebuild runs aventara:prepare again, then next build
npm start              # next start
  • aventara:prepare ends with aventara-build --client, so the build compiles the Contracts and writes the browser client (src/aventara/client.ts, src/aventara/universal.ts, src/aventara/generated/) from the same configuration. Nothing has to run first, and there is no frontend client to commit or regenerate separately.
  • src/aventara/server.ts passes prebuilt: aventaraContracts when NODE_ENV is production, so next start loads the Contracts instead of compiling them. In CI, npx aventara-build --check --client exits 1 when a generated file is stale.
  • src/instrumentation.ts awaits ready(), so a bad configuration fails next start instead of the first request. Keep it: @/aventara/universal needs it on the server.
  • Keep serverExternalPackages: ["@aventara/core", "@aventara/prisma7-adapter"] in next.config.ts, so every route and page shares one core and one Prisma client (Next.js host).
  • DATABASE_URL comes from the environment; Next.js also loads .env. src/prisma.ts refuses a missing one on first use.
  • The Edge runtime is not supported. Keep Aventara's routes and pages on the Node.js runtime (the default).
  • Aventara needs no proxy file (proxy.ts). If your own proxy reads request bodies, Next.js's limit on the bodies it buffers there (10 MB by default) applies before limits.maxRequestBytes does.
  • The browser calls the API on its own origin, so there is no CORS to configure.

Behind a proxy

  • The protocol path is api (/api), regardless of Nest's global prefix. Route /api/* to the server.
  • Pass Aventara-* and Authorization headers through. If your proxy assigns a request id, have it set Aventara-Request-Id (not X-Request-Id).
  • A proxy's own body limit and the framework's maxRequestBytes both apply: the stricter one wins.

Checklist

  • DATABASE_URL and PORT set in the environment; .env not committed.
  • aventara:prepare runs before build (install scripts enabled, or run it yourself).
  • On serverless or edge: aventara-build --check in CI, and prebuilt: aventaraContracts where the framework is created.
  • prisma migrate deploy runs before the new server starts.
  • A health route outside /api/_*, and app.enableShutdownHooks().
  • CORS_ORIGINS set to the deployed frontend's origin, if a browser on another origin calls the API.
  • A rate limiter in front of, or in, the server.
  • Guards and limits set for the client layer: Authentication and guards, Limits and safety.
  • The frontend client regenerated and committed from the deployed contract (a separate frontend; a Next.js app builds its own).
  • Next.js: serverExternalPackages kept, instrumentation.ts awaiting ready(), aventara-build --check --client in CI.

See also