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.
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
| Piece | What it is | Needed |
|---|---|---|
| PostgreSQL | Version 15 or newer | Always |
| The app process | Your template's own start command | Always |
| A migration step | questpie migrate, before the app rolls | Once you commit a migration |
| A worker process | A 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 key | QUESTPIE name | Standard name | With neither |
|---|---|---|---|
app.url | QUESTPIE_APP_URL | APP_URL | http://localhost:3000 |
db.url | QUESTPIE_DB | DATABASE_URL | Throws |
secret | QUESTPIE_SECRET | BETTER_AUTH_SECRET | Undefined |
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 appquestpie 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.
| Slot | With no configuration | What breaks |
|---|---|---|
storage | Local disk at ./uploads | A redeploy wipes it. Replica B cannot read it. |
kv | A Map in this process | Two replicas disagree about every key. |
email | ConsoleAdapter, in dev only | Nothing 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
| Topic | Page |
|---|---|
runtimeConfig, app config, auth config, modules | Configuration |
| Declaring and validating variables, server and client | Environment |
| Writing a migration, and applying it without downtime | Migrations |
| More than one app process, and more than one worker | Scaling |
| Probes, traces, metrics and logs | Monitoring |
Next
Configuration covers questpie.config.ts and the
files beside it. That is where every slot above gets set.