Skip to content

How to: a local Avoca dev database (avoca-dev)

A step-by-step guide to standing up a private, local avoca-next database that behaves like production — so you can run the app, iterate on migrations, and clone real teams without touching prod or the shared staging DB.

The tool is a standalone repo, lazer-sandyc/avoca-local-db — one avoca-dev bash CLI plus a setup.sh onboarding script.

Who this is for

Any Avoca engineer who wants to develop avoca-next against a real-schema database they fully control. Migrations you run here only touch your machine; they reach staging/prod through the normal PR + deploy pipeline.

The sequence, top to bottom

The whole happy path, once, in order. Each step links to its detail section below.

#StepCommand
1Install Supabase (+ a Docker engine + psql/pg_dump)brew install supabase/tap/supabase§2
2Pull the repogit clone git@github-avoca:lazer-sandyc/avoca-local-db.git && cd avoca-local-db§3
3setup.sh — first run creates config.sh and stops./setup.sh§3
4Update env — set your paths in config.sh + creds in ~/.avoca/postgres.env, then re-run to build + start the local DB$EDITOR config.sh && ./setup.sh§4
5Seedoptional Test Auto transfer teamsavoca-dev seed§11
6Types — generate Supabase types from localavoca-dev types <worktree>§6
7setdev + bring up the appavoca-dev db setdev <worktree> then pnpm dev§5
8Duplicate a team — config + its last week of real callsSOURCE_DB=production avoca-dev duplicate-team <id>§7
9Run a migrationavoca-dev migrate up <worktree>§6

Steps 1–4 are one-time onboarding. Steps 5–9 are the per-worktree loop (and you re-run types after each migration). Everything below expands these.

Where does the database itself get started?

The "database" is the local Supabase stack (Postgres in Docker). You don't start it as a separate step the first time — setup starts it and snapshots staging into it (it has to be running to load data). It then stays up until you stop it or reboot.

On a later session, the stack is stopped — bring it back before steps 5–9:

sh
avoca-dev db up        # start the stack   (avoca-dev status shows if it's already up)
avoca-dev db down      # stop it (keeps data)
avoca-dev db cycle     # restart if it wedges

1. The mental model

Your local database is assembled from three sources, each chosen so local matches what you'd expect from production without the risk or the bulk:

LayerComes fromWhy
Schema + baseline dataa snapshot of stagingStaging's app schemas are small (~tens of MB) and carry no real PII (calls/customers are empty), so we clone them wholesale. Schema-accurate, safe.
Global config (voices, LLM models, transcribers, feature flags)prod, synced on setupStaging is sparse on these; prod is the source of truth. This is what makes flags like enterprise_call_flows light up locally the way they do in prod.
A specific real team's configprod, on demand (duplicate-team)When you need a realistic team (agents, blueprint, transfer destinations) to test against — copied config-only, PII/provider-handles scrubbed.

Everything is read-only against prod/staging (schema dump + filtered selects), reversible, and disposable. Auth is a fresh local Supabase (GoTrue) — staging/prod auth is never copied; you log in as a locally-seeded user.

2. Prerequisites

Install the Supabase CLI (macOS / Homebrew):

sh
brew install supabase/tap/supabase          # runs the local stack

You also need psql + pg_dump (Postgres 15+). Check first — which psql pg_dump. If you already have them (a postgresql@NN formula or Postgres.app), you're set. Only if they're missing:

sh
brew install libpq && brew link --force libpq

libpq won't link?

If brew link libpq errors that a postgresql@NN already owns psql/pg_dump/clusterdb, that just means you already have those binaries — skip libpq. Don't --overwrite; it hijacks your existing Postgres client for no benefit.

Then make sure you have:

  • A Docker engine running (Docker Desktop, OrbStack, colima — any). The Supabase CLI runs the stack as containers; there is no non-Docker mode.
  • An avoca-next clone (for the migration files + worktrees) — wherever you keep it.

Your personal Postgres creds (the one thing you set up yourself)

Avoca's own tooling reads a file at ~/.avoca/postgres.env holding your personal STAGING_POSTGRES_URL and PROD_POSTGRES_URL. These are per-developer credentials, so they can't ship with any repo — you have to get them from Jackson. He issues them through a 1Password share: check your email first (he may have already sent yours); if not, message him and ask for your local Postgres creds.

Once you have them, create the file (this is Avoca's standard location — the same one pnpm setup:staging-dev uses):

sh
mkdir -p ~/.avoca && chmod 700 ~/.avoca
cat > ~/.avoca/postgres.env <<'EOF'
STAGING_POSTGRES_URL=postgres://...   # from Jackson's 1Password share
PROD_POSTGRES_URL=postgres://...       # from Jackson's 1Password share
EOF
chmod 600 ~/.avoca/postgres.env

The tool only ever reads this file — it never creates or writes it.

3. First-time setup

Clone the tool wherever you keep your repos (there's no required location), and run the onboarding script:

sh
git clone git@github-avoca:lazer-sandyc/avoca-local-db.git
cd avoca-local-db
./setup.sh

The first run creates your config.sh and stops — so you can point it at your machine before it builds anything (repo layouts differ from person to person). Set your paths (see §4), then run it again:

sh
$EDITOR config.sh      # set AVOCA_NEXT_DIR + WORKTREES_DIR to your paths
./setup.sh             # now it builds

The build (via avoca-dev setup) does — in order:

  1. Starts the local Supabase stack (excluding the flaky/unneeded analytics containers).
  2. Snapshots staging into it — schema + data for the app schemas.
  3. Grants the Supabase roles (anon/authenticated/service_role) that a pg_dump mirror strips.
  4. Seeds the umzug ledger from origin/main, so migrate only ever runs unmerged migrations.
  5. Syncs global config from prodvoices, llm_models, transcribers, feature_flags.
  6. Creates the synthetic owner/enterprise (so duplicate-team works with no extra step).
  7. Creates your login user (avoca-user@avoca.ai — an @avoca.ai address = admin).

It's idempotent — safe to re-run. When it finishes you have a working, prod-like local DB and can log in (avoca-user@avoca.ai / avoca-pass).

4. config.sh — your settings, and how to change them

config.sh is your per-machine settings file. setup.sh created it by copying config.example.sh; it's gitignored, so it's yours alone — edit it freely.

How to change a setting. It's a plain shell file. Each setting is one line:

sh
: "${AVOCA_NEXT_DIR:=/Users/you/code/avoca-next}"

The : "${VAR:=value}" form means "use value unless it's already set." To change a setting, edit the value between the braces. Precedence is env var > config.sh > built-in default, so you can also override any setting for a single command without editing the file:

sh
SOURCE_DB=production avoca-dev duplicate-team 3212

The paths you MUST set — the example values are only examples; there is no standard layout, so point them at your repos:

  • AVOCA_NEXT_DIR — the absolute path to your avoca-next clone. avoca-dev reads its origin/main to seed the migration ledger, so it has to be right — setup.sh refuses to build if it isn't a real avoca-next clone.
  • WORKTREES_DIR — where your avoca-next worktrees live. Worktree commands (db setdev, migrate, types) take a <slug> and resolve it to $WORKTREES_DIR/<slug>.

Everything else has a working default — the local stack lives in this repo, login is avoca-user@avoca.ai / avoca-pass. Most settings are re-read on every command, so you can edit config.sh anytime and the next command picks it up (the exception is LOGIN_*, which only apply when the login user is created — re-run avoca-dev seed to apply a change).

Other settings you might touch:

SettingDefaultWhat it does
SOURCE_DBstagingWhich upstream the snapshot reads. production does a schema-only prod mirror instead.
REFERENCE_SOURCEproductionWhere global config (SYNC_CORE) is synced from — prod, because staging is sparse.
LOGIN_EMAIL / LOGIN_PASSWORDavoca-user@avoca.ai / avoca-passYour local login (email stays @avoca.ai for admin). Change + re-run avoca-dev seed.
SUPABASE_EXCLUDElogflare,vectorContainers skipped on start (the analytics stack flakes on cold start).

5. Run the app against local

The tool configures your worktree; you start the app the way you normally do.

sh
# point the worktree's env (apps/web + apps/dashboard) at the local DB
avoca-dev db setdev <worktree>

# then start it however you normally would, from the worktree
cd $WORKTREES_DIR/<worktree>
pnpm dev

Open the app at http://localhost:3000 and log in with your seeded @avoca.ai user at /signin?auth=password.

setdev handles the prod/local split-brain — but you must restart

Two layers read the DB from different env vars: the server (API routes, crons) reads POSTGRES_*; the browser (the calls list, etc.) reads NEXT_PUBLIC_SUPABASE_URL, which Next.js inlines into .next at build time. Older setups left the Vercel-pulled prod NEXT_PUBLIC_SUPABASE_URL live alongside the local one, so the browser read prod while the server read local — the list showed prod calls, and clicking one 404'd against the empty local DB.

db setdev now fixes this automatically: it comments out the prod originals it overrides (a reversible #avoca-dev-off# tag; setprod restores them byte-for-byte) and wipes apps/web/.next + apps/dashboard/.next on every switch. Two things are still on you: the dev server reads env at startup, so restart it after any setdev/setprod; and your browser caches the old bundle, so hard-reload once (⌘⇧R). Pick one host — localhost or 127.0.0.1 — and stick to it (different cookie origins).

6. Work on a migration (the cheap local loop)

The whole point of a private DB: a migration is cheap to iterate while unmerged, and only becomes immutable once it deploys.

sh
# 1. write / edit the migration in your avoca-next worktree
$EDITOR $WORKTREES_DIR/<worktree>/packages/db/migrations/<ts>_<name>.sql

# 2. apply it to LOCAL (only unmerged migrations run — the ledger tracks origin/main)
avoca-dev migrate up <worktree>

# 3. regen Supabase types from the LOCAL schema, so code sees the new columns pre-merge
avoca-dev types <worktree>

# 4. test. Not happy with the migration? While it's unmerged, EDIT THE SAME FILE and
#    reapply from a clean slate:
avoca-dev reset          # rebuild from staging + reseed the ledger from origin/main
avoca-dev migrate up <worktree>

How it knows what to run: migrate reads the .sql files in that worktree's packages/db/migrations/, compares them to the local ledger (migrations.umzug_migrations), and applies only the ones not yet recorded — in timestamp order. Since the ledger was seeded from origin/main, the only pending file is your branch's unmerged migration.

Convert to a PR: lint (pnpm --filter @avoca/db db:lint), commit the migration + the locally-regenerated types, open the PR. While it's unmerged the file is still yours to edit (respond to review by amending it). Once it merges and deploys, it's frozen — further changes are new migrations.

Keep local current with prod: as prod merges migrations, git merge origin/main in your worktree then avoca-dev migrate up applies the newly-merged ones. Or avoca-dev reset for a full re-mirror.

7. Clone a real team (with its recent calls)

Staging has almost no teams. When you need a realistic one to test against, copy a team from prod:

sh
SOURCE_DB=production avoca-dev duplicate-team <team_id>

This copies two things:

  • Config — agents, voice assistants, transfer destinations, responder (SMS) config, variables, and the modular blueprint it uses — re-attached to the synthetic owner (no owner PII), with vapi_assistant_id nulled and phone numbers dropped. PII-free, and it does not touch Twilio (buy a test number in the UI if you actually need to place a call).
  • Its last week of real calls — by default — so the calls page has data and the linking/reconciliation path has rows to inspect (calls, call_flows, transcripts/analysis, and the transfer_destination_logs notes). This is real call content (transcripts, caller numbers), imported local-only, never shared. Recording audio is not mirrored (Storage isn't copied), so full recording-stitch has no media to assemble.

Tune or skip the call import:

FlagEffect
(none)config + last 7 days of calls
--no-callsconfig only (no call PII)
--since <days>widen the call window
--limit <n>cap the import (a busy team's week can be large)

The call import only pulls data when the source is prod (SOURCE_DB=production) — staging is call-empty, so on the default staging source it's a harmless no-op.

Add calls to an already-cloned team with the standalone command:

sh
SOURCE_DB=production avoca-dev dup-calls <team_id> [--since <days> | --limit <n>]   # default: last week

Find the team_id in the Avoca dashboard URL, or ask an agent to look it up by name. Done with a cloned team? avoca-dev delete-team <id> purges it.

8. Global config & feature flags (why local matches prod)

Ever notice a nav item or feature that's on in prod but missing locally? It's usually a feature flag. Avoca resolves flags from feature_flags (global registry, with an avoca_override) and feature_flag_targets (per-team/enterprise). setup syncs feature_flags from prod so those resolve like they do in production.

The synced set is SYNC_CORE in avoca-deva list that grows. When a prod-only global setting bites you locally, add its table to SYNC_CORE, and everyone picks it up on the next git pull. Re-sync anytime with avoca-dev reference.

9. Managing the stack

sh
avoca-dev db up       # start the stack (handles the -x excludes for you)
avoca-dev db down     # stop it (keeps data; --no-backup wipes)
avoca-dev db cycle    # stop + start — the fix when it wedges (e.g. connection saturation)
avoca-dev status      # stack + which DB each worktree points at

If the DB stops responding (a hung query or the app exhausting the connection pool), avoca-dev db cycle clears it.

10. Reset / start fresh

sh
avoca-dev reset       # wipe the DB + rebuild from scratch (fresh staging snapshot + global config)

For a pristine new-teammate simulation (fresh containers + config): supabase stop --no-backup in the stack dir, rm -rf supabase config.sh, then ./setup.sh.

11. Command reference

CommandWhat it does
avoca-dev setupStand up local Supabase, snapshot staging, sync global prod config, create owner + login. Idempotent.
avoca-dev seedOptional synthetic Test Auto teams (English/Spanish agents for transfer tests). Not part of setup.
avoca-dev referenceRe-sync global config (SYNC_CORE + extras) from prod.
avoca-dev duplicate-team <id>Copy a team's config from prod (or staging) into local + its last week of real calls. --no-calls / --since N / --limit N to tune.
avoca-dev dup-calls <id>Import real calls into an already-cloned team (default: last week). --since N / --limit N. Needs SOURCE_DB=production.
avoca-dev delete-team <id>Remove a cloned team from local.
avoca-dev db up|down|cycleStart / stop / restart the local Supabase stack.
avoca-dev db setdev|setprod|status [wt]Point a worktree at local / prod, or show which.
avoca-dev migrate [up|pending|executed|down] [wt]Run a worktree's unmerged migrations against local (hard-pinned to local).
avoca-dev types [wt]Regen Supabase types from the local schema.
avoca-dev resetWipe + rebuild from scratch.
avoca-dev statusStack + DB + worktree pointers.

12. Gotchas

  • Cold-start container flake. The analytics stack (logflare/vector) comes up unhealthy on a cold start and aborts the whole thing — hence SUPABASE_EXCLUDE=logflare,vector. avoca-dev db up handles it.
  • .next split-brain (see §5) — setdev/setprod now auto-fix the duplicate env + wipe .next; you just restart the dev server (it reads env at startup) and hard-reload the browser after a switch.
  • Imported calls carry real PII — transcripts and caller numbers land in your local DB (local-only, never shared). Recording audio isn't mirrored, so recording-stitch has nothing to assemble locally.
  • Empty dropdowns are usually a missing global table — add it to SYNC_CORE (§8).
  • The DB is one stack shared by every worktree. Teardown is per-team (delete-team), not per-worktree.
  • Prod is only ever read, always read-only + time-bounded + name-tagged; there's a local audit log at ~/.avoca-dev/source-reads.log.