QUESTPIE

Deploying

A QUESTPIE deploy is a PostgreSQL database, one process serving HTTP, a migration step in front of the rollout, and a worker if you wrote jobs. These pages cover each piece, and the parts you own yourself.

View markdown

The app you run on your laptop is the app you run in production. What changes is where the values come from, who applies the schema, and how many copies run.

What a deploy is made of

PieceWhat it isNeeded
PostgreSQLVersion 15 or newerAlways
The app processYour template's own start commandAlways
A migration stepquestpie migrate, before the app rollsOnce you commit a migration
A worker processA file that calls app.queue.listen()Only if you wrote jobs

QUESTPIE ships no server. It gives you createFetchHandler from questpie/http. Every template mounts that behind a server QUESTPIE does not provide. So the start command is whatever your template's package.json says.

That version floor is checked, not just documented. The database service reads server_version_num on connect. Below 15 it throws and names the version it found.

The worker is a separate process on purpose. A job that saturates the CPU would otherwise slow every request on that instance. You write the worker file yourself. Workers covers it and its options.

What it reads from the environment

Leave app, db or secret out of runtimeConfig() and it resolves them for you. It reads the QUESTPIE name first, then the standard one.

Config keyQUESTPIE nameStandard nameWith neither
app.urlQUESTPIE_APP_URLAPP_URLhttp://localhost:3000
db.urlQUESTPIE_DBDATABASE_URLThrows
secretQUESTPIE_SECRETBETTER_AUTH_SECRETUndefined

An explicit value always wins. The database throw fires while questpie.config.ts is evaluated. A missing URL fails at boot, not at the first query.

storage is read from the environment too. QUESTPIE_STORAGE_ENDPOINT, QUESTPIE_STORAGE_BUCKET, QUESTPIE_STORAGE_ACCESS_KEY and QUESTPIE_STORAGE_SECRET_KEY together give you an S3 adapter. Set the endpoint but miss one of the rest, and it warns, then falls back to local disk. The templates pass storage explicitly, so none of this fires for them.

Set the secret before you set `NODE_ENV=production`

With no secret at all, Better Auth falls back to its own published default. Under NODE_ENV=production it refuses that value and throws at startup. Generate one with openssl rand -base64 32. Use a different value per environment.

The templates declare these again in src/lib/env.ts, with @t3-oss/env-core and a Zod schema. That file is where you add your own variables. Environment covers declaring them, and getting the public ones into the browser bundle.

Applying the schema

Migrations are files you commit. Applying them is its own deploy step. Run it once, before the new version serves.

questpie migrate   # apply pending migrations, then exit
bun run start      # then roll the app

questpie migrate is an alias for questpie migrate:up. It applies your pending migrations, then runs the search adapter's own index migrations. With no migration files at all it prints "No migrations found" and stops. The search step is skipped too. Keep it out of the boot path. A failed schema change is then one step you can read, not a replica that crash-loops.

questpie push is the other command, and it is for development. It diffs your schema straight into the database and records nothing. It warns and runs either way. --force only drops two lines of that warning. Point it at a local database and nothing else.

`migrate` tries to create an extension

The Postgres search adapter's migrations include CREATE EXTENSION IF NOT EXISTS "pg_trgm", and pgvector's include "vector". A database user without that privilege fails the step, and the error names the statement. Create the extension once during provisioning.

Defaults a second replica breaks

db.url is the only slot with no fallback. Every other slot has one, and three of those fallbacks are local to a single process.

SlotWith no configurationWhat breaks
storageLocal disk at ./uploadsA redeploy wipes it. Replica B cannot read it.
kvA Map in this processTwo replicas disagree about every key.
emailConsoleAdapter, in dev onlyNothing here. It throws in production instead.

Email is the one that fails loudly rather than quietly. With no adapter, a send prints to stdout in development and throws in production. The app still starts either way, so an app that never sends mail needs no email config at all. The templates set ConsoleAdapter explicitly, so swap it before you ship.

Realtime is the one that is already fine. The default change broker is PostgreSQL LISTEN/NOTIFY on your db.url. Two replicas see each other's events. Infrastructure covers every slot and the adapters that ship for it.

What you own

QUESTPIE does none of the following. Check each one before your first deploy.

  • TLS. Terminate it in front of the app.
  • Rate limiting. QUESTPIE throttles none of your routes. Better Auth throttles its own /auth/* routes in production, counting in memory per process. Put the rest at the edge.
  • Process supervision. Use your platform's restart policy.
  • Secret storage. The app reads its secrets from the environment. Filling that environment is yours.
  • Backups. See your database provider.

Where each topic lives

TopicPage
runtimeConfig, app config, auth config, modulesConfiguration
Declaring and validating variables, server and clientEnvironment
Writing a migration, and applying it without downtimeMigrations
More than one app process, and more than one workerScaling
Probes, traces, metrics and logsMonitoring

Next

Configuration covers questpie.config.ts and the files beside it. That is where every slot above gets set.

On this page