Skip to main content
A local checkout runs the entire product: the chat app, the Receipt runtime, Factory and its sandboxes, and the PostgreSQL database everything is built on. You choose how much to start — a fast web loop with hot reload, or the full supervised stack that behaves like a deployment. Two cooperating products share one database:
  • The web app, apps/start — TanStack Start + Vite + React with Nitro, Rocicorp Zero for sync-first client state, and Better Auth for users and organizations.
  • The Receipt runtime and Factory, packages/receipt-app — the event-sourced agent orchestration plane.
Every entry point below starts some combination of the two. Read the prerequisites first: each one is there because a specific check or process fails without it.

Prerequisites

Required for any local run

Never run a bare bun for repo commands. Use ./bunw (bunw.cmd on Windows), which is the only thing that guarantees the exact pinned version.

Also required for the full supervised stack

start:all and local:up start two more things. uvx is checked in the start:all preflight; the Resonate binary is resolved and version-checked later, by the runtime supervisor, as it spawns the broker.
The name you install the Resonate binary under changes the code path. The supervisor resolves it as RESONATE_BIN, then resonate-server on PATH, then resonate on PATH, then the literal string resonate-server. When the basename is resonate-server, the supervisor always emits the legacy flag set (serve --server-bind …) and returns early from the >= 0.9.7 version assertion, skipping it entirely. Installing it as resonate is what actually exercises both the version check and the probe that selects the modern flags.

Required for agent runs

codex (@openai/codex) is what executes Factory agent work, and receipt doctor treats a missing codex as a blocking failure. The runtime container image pins 0.130.0.

Optional, feature-specific

validate:stack, the whole-stack harness, also needs python3, mktemp and df, and enforces a floor of 8192 MiB free disk (VALIDATE_STACK_MIN_FREE_DISK_MB) when it starts the stack itself. See Testing, simulation, and stack validation.

Install commands, as the repository writes them

Linux release assets are resonate_linux_x86_64.tar.gz and resonate_linux_aarch64.tar.gz. Note that the line above installs the binary as resonate-server — the name that takes the legacy flag path and skips the version assertion.

./bunw, the toolchain wrapper

./bunw resolves the expected Bun version from .bun-version, falling back to the packageManager field in package.json, and searches candidates in this order: RECEIPT_BUN_BINBUN_BIN$BUN_INSTALL/bin/bun → the repo-local .bun/toolchains/<version>/node_modules/.bin/bun.bun/bin/bun~/.bun/bin/bun/opt/homebrew/bin/bun/usr/local/bin/bunPATH. Only a binary whose --version matches exactly is accepted. If none matches, it npm installs Bun into the gitignored .bun/toolchains/<version>/. Your global Bun is never replaced or downgraded. RECEIPT_BUN_AUTO_INSTALL=0 disables that bootstrap, in which case you must set RECEIPT_BUN_BIN yourself; RECEIPT_NPM_BIN overrides which npm performs it. Failure exits with code 127 and prints Set RECEIPT_BUN_BIN=/absolute/path/to/bun to use an existing exact-version binary. Check the toolchain at any time:
The preflight prints exactly [preflight] Environment is ready. on success, or [preflight] Environment is not ready: followed by one - <failure> line per problem, and exits 1.

Bring up a checkout

1

Install the toolchain and dependencies

This installs the pinned Bun 1.3.12 first if your machine does not already have it.
2

Create your environment file

Env files do not perform command substitution, so run openssl rand -base64 32 in your shell and paste each result in as a literal value — do not put $(openssl rand -base64 32) in the file itself. Run it four times, once per key, and paste each output into the corresponding line:
Use a separate generated value for each of the four keys. The credentials in ZERO_UPSTREAM_DB are the ones the bundled Postgres compose file creates. ZERO_UPSTREAM_DB is the single canonical database variable and deliberately accepts no aliases — accepting them once let the app and the runtime silently point at different databases.Stop here. You do not need a second file; see below.
3

Start Postgres

The bundled compose file runs Postgres 16 as postgres -c wal_level=logical, which Zero’s logical replication requires. A Postgres you brought yourself probably runs with the default wal_level=replica and will not sync.
4

Compile translations — before the database reset

This is mandatory before the reset. web:db:reset spawns the Better Auth CLI migrator, whose config imports application modules that import the generated Paraglide bundle; without it the reset fails on an import. ./bunw run dev compiles i18n itself through its predev hook, so this step exists only for the reset ordering.
5

Reset the database

This is destructive: it drops the zero_data publication and every base table in public, runs the Better Auth migrations, then runs the Zero reset. For the non-destructive, forward-only path, see Database and migrations.
6

Run

Then open http://localhost:3000.

One environment file, not two

apps/start/.env.example carries the line “For Local Development, Zero requires separate ‘.env’ file”, because zero-cache starts as bun --env-file=.env --env-file=.env.local zero-cache-dev — Bun’s own env-file loader rather than the repo’s. That comment is stale. apps/start/.env is not required. Nothing in the repository creates it, it is gitignored, and no checked-in code requires it; ZERO_UPSTREAM_DB belongs in .env.local with everything else.
dev, start:all and the receipt CLI read the same four files in the same order — root .env, root .env.local, apps/start/.env, apps/start/.env.local — and in all three a value already exported in your shell wins over all four. Which file wins when the same key is set twice is not the same in every loader, so keep each variable in exactly one file. validate:stack is the exception in both respects: it sources the same four files with set -a, so there the last file wins and overrides your shell. For the variables themselves, see Configuration.

The three run modes

dev runs through a Turbo TUI, which needs a real terminal; in a non-interactive shell run ./bunw run --cwd apps/start dev instead. dev and local:up cannot run at the same time — they want the same ports. local:up takes one positional argument, the provider, which must be opensandbox (alias computer). Anything else prints usage and fails with Unknown provider '<x>'. Use opensandbox.

local:up resolves everything from the shell, and never reads .env.local

This is the single most surprising rule on this page. local-up.sh does not read apps/start/.env.local at all. It resolves every value from the shell, falls back to repo defaults, writes a generated env file under .deploy-artifacts/local-up/, exports it, and execs the supervisor — which skips any key already present in the environment. Those exported fallbacks therefore outrank your file. Source the file into the shell first:
Docker Compose behaves the same way: the Postgres compose file publishes "${START_ALL_POSTGRES_PORT:-5432}:5432", and Compose reads the shell, not any env file.

What local:up does that start:all does not

  • A conditional, destructive database reset. A schema probe calls the database ready only if user, organization, receipt_chat_context_projection, receipt_chat_history_projection and receipt_receipts all exist, the legacy threads and messages tables do not, and the publication zero_data exists. If the probe fails it runs web:db:reset, which drops your data. LOCAL_UP_DB_RESET=0 disables the reset, =1 forces it, auto (the default) uses the probe.
  • A locally minted Connect token. It looks for an organization holding an OpenAI BYOK row, preferring one with a valid aws connection secret, and mints a RECEIPT_CONNECT_TOKEN with a 12-hour TTL into the generated env file, logging Using local BYOK organization <org>.
The supervisor then rebuilds the Zero native binding if needed, preflights every port on both IPv4 and IPv6, starts the containers, builds and starts the OpenSandbox controller, pre-migrates the Receipt durable schema before running Zero migrations and refreshing the zero_data publication, builds the web bundle, and starts the runtime, zero-cache and gateway. Success is the log line start_all.ready Local stack is up.

Ports

RESONATE_URL is not a port control. It is the address the runtime roles dial, the supervisor derives it from the broker port, and setting it to a port other than RESONATE_PORT makes the supervisor treat the broker as external and start none at all. start:all asserts free, on both 127.0.0.1 and ::1, only the gateway, web internal, zero-cache, zero-cache + 1, receipt, resonate and resonate metrics ports, plus opensandbox, slack and teams when those are enabled. Postgres, Redis and the integrations ports are deliberately not preflighted, which is why leaving those containers running between restarts is safe. dev instead kills stale repo-owned listeners on its ports and errors only on a foreign one — Port <n> is already in use by a non-repo process; stop it or change the port.

Moving the web port without breaking sign-in

BETTER_AUTH_URL and the browser-bundled VITE_BETTER_AUTH_URL must share the origin you actually browse to. VITE_BETTER_AUTH_URL is inlined into the bundle at build time, so a mismatch does not fail loudly — sign-in breaks with CORS errors. Move all four together and rebuild:
For Postgres: START_ALL_POSTGRES_PORT=5433 docker compose -f docker-compose.postgres.yml up -d.

Stopping the stack

There is no local:down script.
Never kill -9 the supervisor. Children are spawned detached, so SIGKILL runs no cleanup handler and the whole tier survives, reparented to PID 1 — ports still bound, health endpoints still answering 200 — and the next local:up fails its port preflight while the stack that answers you is the old one.
  • Stop with Ctrl-C in the local:up terminal, or pkill -f 'bun \./scripts/start-all\.mjs', which sends SIGTERM. The supervisor registers SIGINT, SIGTERM and exit handlers and kills each child’s process group.
  • pkill -f local-up.sh is not a substitute: local-up.sh installs no trap and execs the supervisor in a subshell.
  • Killing any non-optional child — a runtime role, Resonate, the gateway, the web tier — takes the whole stack down through the supervisor’s failure path.
  • Zero forks its own workers detached, so change-streamer.js, which holds the zero-cache port + 1, can outlive shutdown. Reap it with pkill -f 'zero-cache/src/server/' after the supervisor has stopped.
  • Cleanup makes no Docker calls, so Ctrl-C never stops the containers — leaving them up between restarts is the intended fast path. Stop them with docker compose -f docker-compose.postgres.yml stop postgres and docker compose -p receiptfactory-local -f docker-compose.local.yml stop redis nango.
docker compose -f docker-compose.postgres.yml down -v deletes the named volume receipt_postgres_data — the whole database, including users, organizations, receipts, projections and encrypted BYOK keys, plus the integrations database with its saved connections. docker system prune -a also deletes the local OpenSandbox worker image and costs a multi-minute rebuild. Neither belongs in a routine stop.
Next step: work through what breaks and why.