- 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.
Prerequisites
Required for any local run
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.
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
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_BIN → BUN_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/bun → PATH.
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:
[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
2
Create your environment file
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: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
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
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
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
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:
"${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_projectionandreceipt_receiptsall exist, the legacythreadsandmessagestables do not, and the publicationzero_dataexists. If the probe fails it runsweb:db:reset, which drops your data.LOCAL_UP_DB_RESET=0disables the reset,=1forces 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
awsconnection secret, and mints aRECEIPT_CONNECT_TOKENwith a 12-hour TTL into the generated env file, loggingUsing local BYOK organization <org>.
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:
START_ALL_POSTGRES_PORT=5433 docker compose -f docker-compose.postgres.yml up -d.
Stopping the stack
There is nolocal:down script.
- Stop with Ctrl-C in the
local:upterminal, orpkill -f 'bun \./scripts/start-all\.mjs', which sends SIGTERM. The supervisor registersSIGINT,SIGTERMandexithandlers and kills each child’s process group. pkill -f local-up.shis not a substitute:local-up.shinstalls no trap andexecs 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 withpkill -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 postgresanddocker compose -p receiptfactory-local -f docker-compose.local.yml stop redis nango.