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.exampleAll 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.
The resolver
Section titled “The resolver”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:
source "$REPO_ROOT/scripts/load-env.sh" "$ENV_NAME"# → CARABASE_ENV, DATABASE_URL, HOST_MASTER_KEY, etc. now in scopeFor 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.
First-time setup
Section titled “First-time setup”cp .env.dev.example .env.devcp .env.staging.example .env.staging# fill in HOST_MASTER_KEY in each (different keys per env!)
pnpm setup:envs # creates all three DBs + runs migrationspnpm db:seed:dev # small starter fixturepnpm 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 reset guardrail
Section titled “The reset guardrail”The database reset routine classifies the DATABASE_URL by the db name suffix:
*_prod/*_production→ prod, refuses to run*_staging→ staging, runs*_dev→ dev, runs- anything else → unknown, warns but runs (legacy
carabasedb)
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.
The test-DB guard
Section titled “The test-DB guard”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:
createdb carabase_testDATABASE_URL="postgresql://…/carabase_test" pnpm db:migratepnpm 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.
Per-env pnpm scripts
Section titled “Per-env pnpm scripts”| 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.
Different master keys per env
Section titled “Different master keys per env”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.