QUESTPIE

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.

View markdown

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:push

That 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:create

The 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:status

migrate: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 migrate

That 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.

VariableDefault
DATABASE_URLNone. The app throws without it.
APP_URLhttp://localhost:3000
BETTER_AUTH_SECRETUndefined. 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.

RequirementVersionHow it is enforced
Bun1.3Nothing checks the version. The questpie CLI runs on Bun, and every template Dockerfile pins oven/bun:1.3-alpine.
PostgreSQL15+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:

src/questpie/server/questpie.config.ts
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.

On this page