# Deploying (/docs/ship)

---
title: Deploying
description: 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.
kind: guide
package: questpie
---

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](/docs/code/jobs/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.

<Callout
	type="warn"
	title="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.
</Callout>

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](/docs/ship/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.

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

<Callout type="warn" title="`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.
</Callout>

## 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](/docs/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](/docs/ship/configuration) |
| Declaring and validating variables, server and client | [Environment](/docs/ship/environment)     |
| Writing a migration, and applying it without downtime | [Migrations](/docs/ship/migrations)       |
| More than one app process, and more than one worker   | [Scaling](/docs/ship/scaling)             |
| Probes, traces, metrics and logs                      | [Monitoring](/docs/ship/monitoring)       |

## Next

**[Configuration](/docs/ship/configuration)** covers `questpie.config.ts` and the
files beside it. That is where every slot above gets set.
