Skip to content

Environments (dev / staging / prod)

Carabase runs against three Postgres databases on the same instance:

Env DB name Purpose Safe to reset?
dev carabase_dev Local-loop dev — fast iteration, noisy fixtures ✅ yes
staging carabase_staging Full eval-seeded corpus for smoke testing ✅ yes
prod carabase_prod Your real data on the always-on host ❌ protected

Each env has its own dotfile:

.env.dev ← copy from .env.dev.example
.env.staging ← copy from .env.staging.example
.env.production ← copy from .env.production.example

All three are gitignored; the .example templates are tracked.

Each dotfile carries the secrets that env needs — at minimum HOST_MASTER_KEY (the 256-bit key that encrypts all third-party credentials at rest) and DATABASE_URL. Keep them per-env: a leaked dev key never exposes prod data.

scripts/load-env.sh <env> is the single source of truth for the env → file mapping. It’s sourced (not exec’d) by every script that needs an env:

Terminal window
source "$REPO_ROOT/scripts/load-env.sh" "$ENV_NAME"
# → CARABASE_ENV, DATABASE_URL, HOST_MASTER_KEY, etc. now in scope

For pnpm scripts, the wrapper is scripts/with-env.sh <env> -- <cmd>:

"dev:staging": "./scripts/with-env.sh staging -- <command>"

Dev falls back to plain .env if .env.dev doesn’t exist yet, so you’re not forced to split on day one.

Terminal window
cp .env.dev.example .env.dev
cp .env.staging.example .env.staging
# fill in HOST_MASTER_KEY in each (different keys per env!)
pnpm setup:envs # creates all three DBs + runs migrations
pnpm db:seed:dev # small starter fixture

pnpm bootstrap runs the full first-run flow for one env (default dev): installs dependencies, scaffolds a missing .env.<env>, creates and migrates the database, and seeds demo data. Pass --env staging to bootstrap a different env, or --skip-seed to skip the demo data.

For prod, copy .env.production.example.env.production only on the machine that will actually run prod. Never put it on a dev laptop.

The database reset routine classifies the DATABASE_URL by the db name suffix:

  • *_prod / *_productionprod, refuses to run
  • *_staging → staging, runs
  • *_dev → dev, runs
  • anything else → unknown, warns but runs (legacy carabase db)

The classifier is pure and unit-tested. To override the prod refusal you must explicitly set CARABASE_I_KNOW=1, which logs the bypass alongside the masked URL.

There is intentionally no pnpm db:reset:prod shortcut. Wiping prod can only be done by invoking the reset routine manually with CARABASE_I_KNOW=1 explicitly set — and that bypass is logged alongside the masked URL. The friction is the point.

A test run is the other way to accidentally clobber real data — a pnpm test pointed at the live carabase_dev DB can destroy your config and drop fixture rows onto the real timeline. So every test entry point (the vitest setup chokepoint, the test DB helper, and the smoke harness) refuses to run unless the effective DATABASE_URL names a scratch DB — one ending in _test, _ci, or starting with carabase_smoke_. The escape hatch is CARABASE_TEST_DB_OK=1.

Create the scratch DB once:

Terminal window
createdb carabase_test
DATABASE_URL="postgresql://…/carabase_test" pnpm db:migrate

pnpm test defaults to it when .env.dev doesn’t set DATABASE_URL. If your .env.dev does set one, override it for the test run.

Script What it does
pnpm bootstrap First-run setup for a new install (default dev)
pnpm bootstrap --env staging Same for staging
pnpm setup:envs Create + migrate all three DBs (idempotent)
pnpm dev:dev Run the host against the dev DB
pnpm dev:staging Run the host against the staging DB
pnpm start:prod Run the built host against the prod DB
pnpm db:migrate:<env> Run migrations against a specific env
pnpm db:seed:<env> Run the hand-written seed against a specific env
pnpm db:seed:eval:<env> Run the realistic sam-rivera fixture against dev or staging
pnpm db:reset:dev Truncate + reseed dev
pnpm db:reset:staging Truncate + reseed staging
pnpm backup:<env> Encrypted nightly backup

Why separate databases on the same instance?

Section titled “Why separate databases on the same instance?”

Cheapest blast-radius isolation that still protects prod. A dev query physically cannot SELECT from prod rows — the connection string points at a different database. A DROP DATABASE against the wrong handle could still hurt, but that’s what the backup pipeline is for.

Each .env.<env> has its own HOST_MASTER_KEY. Compromising dev doesn’t leak prod. The pnpm bootstrap flow scaffolds a fresh dotfile per env when you create each one, so keeping the keys distinct happens by default.