Setup
Setup
What it does
Gets the app running locally against a real Postgres database, with or without real Google/Stripe credentials.
How to configure it — environment variables
Copy .env.example to .env and fill in what you need. All parsing and
enforcement is centralized in lib/env.ts — don't read
process.env.* directly in new server code, import { env } from there
instead (two files are deliberately excluded — see its comments).
| Var | Required to build/boot? | Required for |
|---|---|---|
DATABASE_URL | Yes at runtime (build-tolerant — see below) | Any DB query |
BETTER_AUTH_SECRET | Yes at runtime (build-tolerant — see below) | Signing sessions. Generate: openssl rand -base64 32 |
BETTER_AUTH_URL / NEXT_PUBLIC_APP_URL | No | Correct OAuth callback / redirect URLs |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | No | "Sign in with Google" only — email/password works without these |
STRIPE_SECRET_KEY | No (falls back to a placeholder key in lib/auth.ts) | Any Stripe call |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY | No | Client-side Stripe.js, if you add it |
STRIPE_WEBHOOK_SECRET | No | Verifying webhook signatures from stripe listen/production |
STRIPE_PRO_MONTHLY_PRICE_ID, STRIPE_PRO_ANNUAL_PRICE_ID, STRIPE_BUSINESS_MONTHLY_PRICE_ID, STRIPE_BUSINESS_ANNUAL_PRICE_ID | No | Actually completing checkout for a paid tier (see billing.md) |
Nothing is required to npm run build — lib/env.ts detects Next's
build phase (NEXT_PHASE=phase-production-build, set while it imports every
route module to "collect page data") and skips enforcement entirely then, so
CI and a fresh clone never fail to build on missing secrets.
At actual runtime (npm run dev / npm run start), lib/env.ts checks
for DATABASE_URL and BETTER_AUTH_SECRET — the two vars nothing works
without:
npm run dev— missing either just logs a clear one-line warning (⚠️ [env] Missing required environment variable(s): ...); the dev server still comes up so you can work on unrelated pages while filling in.env.npm run start(NODE_ENV=production) — missing either throws, surfacing as a clean500on the first request that importslib/auth.tsorlib/db/index.ts, instead of a confusing rawpg/OAuth error deeper in the stack.
Everything else (Google, Stripe) is optional in every mode — those are features that degrade gracefully, not app-breaking requirements.
How it works — clone to running app in under 2 minutes
Needs Docker + Node — nothing else.
npm install
npm run setup # interactive: prompts for DB/app URL/Google/Stripe,
# writes .env, generates BETTER_AUTH_SECRET, brings up
# Postgres+MinIO, migrates, seeds
npm run dev # http://localhost:3000scripts/setup.ts is the one-command bootstrap — see its own
file comment for exactly what it does and in what order. It's interactive by
default; npm run setup -- --defaults runs the same steps non-interactively
(every prompt takes its default, blank for anything without one), for CI or
scripted first-run. It's also safe to re-run: an existing .env is kept
unless you explicitly choose to overwrite it, and every step past that
(db:migrate, db:seed) is itself idempotent.
If you'd rather do it by hand instead of the prompts:
cp .env.example .env
# generate a real secret and paste it in as BETTER_AUTH_SECRET:
openssl rand -base64 32
npm run dev:db && npm run db:migrate && npm run db:seed
npm run devLog in with a seeded account (see scripts/seed.ts for the full list):
| Password | Role | |
|---|---|---|
admin@example.com | password123 | admin (visit /admin) |
user@example.com | password123 | user, has an active "pro" subscription seeded |
db:seed also seeds 2 contact submissions (visible at /admin/contacts)
and the demo user's subscription (visible at /admin/subscriptions and
/billing).
Re-running npm run setup / npm run db:seed is safe — it's idempotent,
skipping any row that already exists by email/reference instead of erroring
or duplicating.
To promote a real signed-up user to admin instead of using the seeded one:
npm run promote-admin you@example.comPostgres via Docker Compose
docker-compose.yml runs postgres:17-alpine on host port 5434 (not
the default 5432 — chosen to avoid clashing with a locally-installed
Postgres or another project's compose stack already using 5432), with a
named volume so data survives restarts. DATABASE_URL in .env.example
already points at it.
npm run dev:db # brings up postgres + minio, blocks until healthy
npm run dev:db:down # docker compose downIf you'd rather point at a Postgres you already have running elsewhere,
just skip npm run dev:db and set DATABASE_URL to that instance instead
— everything past that (db:migrate, db:seed) works the same either way.
Why
dev:dbis twodocker composecalls, not one. Compose's--waitfails the whole command (exit 1) if any service in the file isn't running/healthy once the wait resolves — includingminio-init, a one-shot container that is supposed to exit 0 after creating the upload bucket (seedocker-compose.yml). A singleup -d --waittherefore reported failure at random even when every container came up fine, taking down everything that chains it (test:integration,test:e2e,npm run setup). Sodev:dbwaits onpostgres+minioonly, then runsminio-initin its own call, whose exit code is that one container's real exit code. Keep that split if you add services.
Scripts reference
| Script | What it does |
|---|---|
npm run dev / build / start | Next.js dev / production build / production serve |
npm run lint | ESLint |
npm run setup | Interactive: prompts, writes .env, dev:db → db:migrate → db:seed — the one-command bootstrap. -- --defaults for non-interactive |
npm run dev:db / dev:db:down | Start/stop the Docker Compose Postgres + MinIO |
npm run db:generate | Diff lib/db/schema/* against the last migration and write a new one into drizzle/ |
npm run db:migrate | Apply pending migrations to DATABASE_URL |
npm run db:push | Push schema directly, skipping migration files (dev-only shortcut, don't use in prod) |
npm run db:studio | Drizzle Studio (visual table browser) |
npm run db:seed | Idempotently seed demo users, a subscription, and contact submissions — see scripts/seed.ts |
npm run promote-admin <email> | Sets user.role = 'admin' for an existing user |
Extend
- Add more seed data — edit
scripts/seed.ts. It's self-contained (rawpg, no@/path aliases — same reasoning asscripts/promote-admin.ts: Node's--experimental-strip-typesdoesn't resolvetsconfigpath aliases) and idempotent by design — check-then-insert per row, keyed on something stable (email,reference_id, etc.) — follow that pattern for new rows so re-runningnpm run setupstays safe. Passwords are hashed viahashPasswordfrom thebetter-auth/cryptosubpath export — that's a real npm package export (unlike@/lib/auth, which pulls in path-aliased imports Node can't resolve here) and produces hashes that verify correctly against the normal login form. - Different Postgres image/version — edit
docker-compose.ymldirectly; update.env.example'sDATABASE_URLport if you change the host port mapping. - Add new required env vars to
.env.examplewith a comment explaining what they're for and whether they're required to boot vs. only for a specific flow — follow the table above as the pattern.
Remove
Setup itself (scripts/setup.ts, db:migrate) isn't something you remove —
it's the flow for the app itself. What's optional:
- Delete
docker-compose.yml,scripts/seed.ts, and thedev:db*/setup/db:seedentries frompackage.json'sscriptsif you don't want the Docker-based flow — pointDATABASE_URLat any Postgres instance and runnpm run db:migratedirectly instead.scripts/setup.tswould need trimming too (drop thedev:db/db:seedsteps it runs).