Deploying
QUESTPIE runs on your servers. Getting there is a migration you commit, three environment variables, a runtime choice, and two health endpoints to point your probes at.
Everything below assumes a project scaffolded by create-questpie and a
PostgreSQL you run yourself.
Choosing push or a migration
Push applies your schema to the database directly. It diffs what the app declares against what the database has, runs the statements, and records nothing.
To push, run the script every template ships:
bun run db:pushThat script is questpie push, from the questpie core. It bypasses migration
history, so a later questpie migrate has no record of what it changed.
push must never touch production
The --force flag only acknowledges the warning the command prints. It adds
no safety check. Point db:push at a local database and nothing else.
A migration is a TypeScript file with an up step and a down step, beside
a snapshot JSON in snapshots/ that it imports. Commit both. QUESTPIE records
each migration it applies in a questpie_migrations table, so every environment
converges on the same schema by the same steps.
Generating a migration
To generate one, change your collections, then run:
bun run migrate:createThe command diffs the current schema against the snapshot chain and writes a
timestamped file into the directory named by cli.migrations.directory. When
nothing changed it prints No schema changes detected and writes no file.
The migration is not live yet. Codegen discovers migration files and emits them
into the generated app, and migrate reads its list from there. Regenerate, then
check what the app now sees:
bun run scaffold:generate
bun run migrate:statusmigrate:status should list your new migration as pending. If it does not, the
file never reached the generated app and applying it will do nothing.
Applying it in production
To apply every pending migration, run:
bun run migrateThat is questpie migrate:up under an alias. Run it as its own deploy step,
before the new version serves. A failed schema change is then one failed step you
can read, rather than a replica that crash-loops.
build({ autoMigrate: true }) runs the same thing during app startup. It suits
development. In production it moves the failure into the boot path of every
replica at once. migrate, migrate:down, migrate:reset and migrate:fresh
take --dry-run, which prints the action and exits without running it. To
reverse the last batch, run bun run migrate:down.
A down migration does not restore data
migrate:down runs the generated down step. If the up dropped a column,
rolling back recreates it empty. For anything destructive, take a backup first
and treat restore as the recovery path.
Setting the environment
QUESTPIE resolves three variables. Only one has no usable default.
| Variable | Default |
|---|---|
DATABASE_URL | None. The app throws without it. |
APP_URL | http://localhost:3000 |
BETTER_AUTH_SECRET | Undefined. Set a distinct value per environment. |
Each also has a QUESTPIE_-prefixed name that wins when both are set:
QUESTPIE_DB, QUESTPIE_APP_URL and QUESTPIE_SECRET. The templates re-declare
these in src/lib/env.ts, where only DATABASE_URL is required.
BETTER_AUTH_SECRET falls back to change-me-in-production there, so a deploy
that forgets it boots anyway.
File storage defaults to local disk
With no storage configured, uploads land in ./uploads inside the container.
That does not survive a redeploy and is not shared between replicas. Set the
four QUESTPIE_STORAGE_* variables for endpoint, bucket, access key and
secret key to get an S3-compatible adapter, and install @aws-sdk/client-s3,
@aws-sdk/lib-storage, @aws-sdk/s3-presigned-post and
@aws-sdk/s3-request-presigner.
Choosing a runtime
Four templates ship, and create-questpie scaffolds any of them: TanStack Start,
Next, Hono and Elysia. Pick by whether you need the admin.
The admin is @questpie/admin, a module you enable. It needs a runtime that
renders, so the scaffolder allows it on TanStack Start and Next and rejects it on
Hono and Elysia. Those two are headless. You get the API and the typed client.
OpenAPI is @questpie/openapi, and the scaffolder pre-selects it on all four.
Every template mounts createFetchHandler from questpie/http itself, so none
of them installs a runtime adapter. The @questpie/next, @questpie/hono and
@questpie/elysia packages exist for wiring QUESTPIE into an app you already
have. All four templates ship a Dockerfile, and all four build on
oven/bun:1.3-alpine.
| Requirement | Version | How it is enforced |
|---|---|---|
| Bun | 1.3 | Nothing checks the version. The questpie CLI runs on Bun, and every template Dockerfile pins oven/bun:1.3-alpine. |
| PostgreSQL | 15+ | A startup check. Below 15 the app throws and names the version it found. |
Extensions are yours to create
QUESTPIE is drizzle-native and never issues CREATE EXTENSION. The starter's
full-text search needs pg_trgm. Local Docker provisions it on first boot. On
managed Postgres, enable it through your provider before the first deploy.
Watching it once it is running
Two routes come from the questpie core with no module to enable. Both are
public, and both sit under the base path your handler mounts, which is /api in
every template. Point each probe at its own:
GET /api/health/live # liveness: 200 while the process runs
GET /api/health # readiness: 200 when ok or degraded, 503 when unhealthy/api/health runs a real query against the database and a real read against KV,
and reports whether search has initialised. Storage and queue are reported as
configured rather than probed. /api/health/live touches nothing at all.
Do not point liveness at /api/health
A liveness probe that depends on the database turns a brief database blip into
a restart loop across every replica at once. That is what /api/health/live
exists to avoid.
For traces, metrics and logs, @questpie/observability is an adapter you pass
into your runtime config. It exports over OTLP/HTTP:
import { otelObservability } from "@questpie/observability";
import { runtimeConfig } from "questpie/app";
import { env } from "@/lib/env";
export default runtimeConfig({
db: { url: env.DATABASE_URL },
observability: {
adapter: otelObservability({
serviceName: "my-app",
otlpEndpoint: "http://collector:4318",
}),
},
});Running it yourself
QUESTPIE is MIT-licensed and ships as packages you install. This page covers the
self-hosted path. The questpie cloud command group deploys to Questpie Cloud
instead.
- The database. PostgreSQL 15 or newer, its extensions, and its backups.
- The host. A container runtime, TLS in front, and a restart policy.
- The deploy step. Nothing applies your migrations unless you run
migrate.
Next
The full concept reference documents every piece of a collection, from field types to access rules.
Running work in the background
A job is one typed background task. You declare it in a file, the generator puts it on app.queue, and a worker drains the queue off the request path.
Guides
One job each, start to finish. Where the reference pages tell you what a thing does, these build something with it and leave you a working result.