> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kentron.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Local development

> Run the whole Receipt stack on your own machine: what to install first, the three run modes, the full port map, and how to stop it cleanly.

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

| Tool                            | Version                                                                               | Why it is required                                                                                                                                                                                                                    |
| ------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Node.js                         | 24.x, pinned by `.node-version` and `.nvmrc`                                          | `./bunw` is itself a Node script, and the repo preflight fails when `node --version` reports a different major. The `start:all` and `local:up` dependency checks both treat a missing `node` as fatal.                                |
| npm                             | ships with Node 24                                                                    | Bootstraps the pinned Bun for `./bunw`, and rebuilds the `@rocicorp/zero-sqlite3` native binding that zero-cache cannot start without.                                                                                                |
| Bun                             | **exactly 1.3.12** — `.bun-version`, and the `packageManager` field in `package.json` | The runtime for every repo command. You do not install it yourself; `./bunw` resolves it or installs it.                                                                                                                              |
| Docker, with the daemon running | any recent                                                                            | Postgres, Redis and the integrations provider run in containers, and every OpenSandbox sandbox is a container. `start:all` requires `docker` when it starts Postgres or local infrastructure; `local:up` requires it unconditionally. |
| curl                            | any                                                                                   | Checked by the `start:all` preflight, and required by `validate:stack`.                                                                                                                                                               |
| git                             | any                                                                                   | A missing `git` binary, or a checkout that is not a git repository, is a **blocking** `receipt doctor` failure.                                                                                                                       |

<Warning>
  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.
</Warning>

### 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.

| Tool                       | Version      | Why it is required                                                                                                                                          |
| -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `uv` / `uvx`               | any recent   | `start:all` requires `uvx` when the OpenSandbox controller is started; it then runs `uvx opensandbox-server`.                                               |
| A `resonate` server binary | **>= 0.9.7** | The runtime supervisor spawns the Resonate broker itself. Its compatibility check throws below 0.9.7, and if the broker exits the supervisor exits with it. |

<Warning>
  **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.
</Warning>

### 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

| Tool   | Needed for                                 | If missing                    |
| ------ | ------------------------------------------ | ----------------------------- |
| `gh`   | GitHub-backed Factory flows                | `receipt doctor` warning only |
| `aws`  | AWS-connected Factory flows                | `receipt doctor` warning only |
| `psql` | An optimization in the Receipt smoke suite | Silently skipped              |

<Note>
  `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](/cli/from-source/testing-and-simulation).
</Note>

### Install commands, as the repository writes them

```bash theme={null}
# uv, which provides uvx
curl -LsSf https://astral.sh/uv/install.sh | sh

# resonate server (macOS arm64 example)
curl -fsSL -o /tmp/resonate.tgz \
  https://github.com/resonatehq/resonate/releases/download/v0.9.8/resonate_darwin_aarch64.tar.gz
tar -xzf /tmp/resonate.tgz -C /tmp
install -m 0755 /tmp/resonate ~/.local/bin/resonate-server
resonate-server --version

# codex
npm install -g @openai/codex@0.130.0
```

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_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 install`s 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:

```bash theme={null}
./bunw run toolchain:bootstrap   # resolve or install the pinned Bun, then print its version
./bunw run toolchain:check       # run the preflight
```

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

<Steps>
  <Step title="Install the toolchain and dependencies">
    ```bash theme={null}
    ./bunw install
    ```

    This installs the pinned Bun 1.3.12 first if your machine does not already have it.
  </Step>

  <Step title="Create your environment file">
    ```bash theme={null}
    cp apps/start/.env.example apps/start/.env.local
    ```

    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:

    ```bash theme={null}
    BETTER_AUTH_SECRET=<paste generated value>
    BETTER_AUTH_URL=http://localhost:3000
    VITE_BETTER_AUTH_URL=http://localhost:3000
    ZERO_UPSTREAM_DB=postgresql://receipt:receipt@localhost:5432/receipt
    VITE_ZERO_CACHE_URL=http://localhost:4848
    BYOK_ENCRYPTION_KEY_B64=<paste generated value>
    RECEIPT_CONNECTION_ENCRYPTION_KEY_B64=<paste generated value>
    RECEIPT_CONNECT_JWT_SECRET=<paste generated value>
    VITE_ENABLE_EMBEDDING=false
    VITE_DISABLE_REDIS=true
    AUTH_EMAIL_PROVIDER=disabled
    ```

    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.
  </Step>

  <Step title="Start Postgres">
    ```bash theme={null}
    docker compose -f docker-compose.postgres.yml up -d
    ```

    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.
  </Step>

  <Step title="Compile translations — before the database reset">
    ```bash theme={null}
    ./bunw run --cwd apps/start i18n:compile
    ```

    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.
  </Step>

  <Step title="Reset the database">
    ```bash theme={null}
    ./bunw run web:db:reset
    ```

    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](/core/database-and-migrations).
  </Step>

  <Step title="Run">
    ```bash theme={null}
    ./bunw run dev
    ```

    Then open `http://localhost:3000`.
  </Step>
</Steps>

### 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.

<Note>
  `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 `source`s the same four files with `set -a`, so there the last file wins and overrides your shell. For the variables themselves, see [Configuration](/core/configuration).
</Note>

## The three run modes

|                                                | `./bunw run dev`                                                                           | `./bunw run start:all`                                                           | `./bunw run local:up`                                                |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Web tier                                       | **Vite dev server with HMR**                                                               | Production Nitro build behind the gateway                                        | Same as `start:all`                                                  |
| Postgres                                       | Started unless `START_DEV_POSTGRES=0`, or `ZERO_UPSTREAM_DB` does not point at `localhost` | Started unless `START_ALL_POSTGRES=0`                                            | Always — `local:up` forces `START_ALL_POSTGRES=1`                    |
| zero-cache                                     | Yes                                                                                        | Yes                                                                              | Yes                                                                  |
| Receipt runtime                                | Yes, with `RECEIPT_SERVER_WATCH=api`, so the `api` role runs under `bun --watch`           | Yes, no watch                                                                    | Same as `start:all`                                                  |
| Resonate broker                                | Yes, supervised by the runtime                                                             | Yes                                                                              | Yes                                                                  |
| Redis                                          | No                                                                                         | Yes                                                                              | Yes                                                                  |
| Integrations provider                          | No                                                                                         | Yes                                                                              | Yes                                                                  |
| OpenSandbox controller                         | No                                                                                         | Yes, unless `START_ALL_OPENSANDBOX=0`; an optional child                         | Yes, and `RECEIPT_FACTORY_COMPUTER_PROVIDER=opensandbox` is forced   |
| Service gateway on 3000 + internal web on 3001 | No — Vite serves 3000 directly                                                             | Yes                                                                              | Yes                                                                  |
| Slack / Teams adapters                         | No                                                                                         | Yes unless `START_ALL_SLACK=0` / `START_ALL_TEAMS=0`; both are optional children | Same                                                                 |
| Production web build                           | No                                                                                         | Yes unless `START_ALL_BUILD_WEB=0`                                               | Yes; `LOCAL_UP_BUILD_WEB` maps to `START_ALL_BUILD_WEB`, default `1` |
| Database reset                                 | No                                                                                         | No                                                                               | **Conditionally, and destructively** — a schema probe decides        |
| Logs                                           | stdio inherited                                                                            | `.deploy-artifacts/start-all/<runId>/<name>.log`                                 | Same                                                                 |

`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 `exec`s 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:

```bash theme={null}
# 1. Load your local configuration INTO THE SHELL. Not optional.
set -a; source apps/start/.env.local; set +a

# 2. The one value local-up.sh cannot derive from anything else.
export LOCAL_UP_ZERO_UPSTREAM_DB="$ZERO_UPSTREAM_DB"

# 3. Optional: skip surfaces you have no credentials for, and allow a slower first boot.
export START_ALL_SLACK=0 START_ALL_TEAMS=0 START_ALL_WAIT_TIMEOUT_SECONDS=300

# 4. Go. Keep this terminal open — it is the supervisor.
./bunw run local:up
```

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

| Service                                | Default port             | Variable that moves it                                                                                                    | Started by                               |
| -------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| PostgreSQL 16                          | 5432                     | `START_ALL_POSTGRES_PORT`, else the port inside `ZERO_UPSTREAM_DB`; `dev` reads only `ZERO_UPSTREAM_DB`                   | `dev`, `start:all`, `local:up`           |
| Web, Vite dev server                   | 3000                     | `PORT`                                                                                                                    | `dev`                                    |
| Service gateway, the public web origin | 3000                     | `START_ALL_GATEWAY_PORT`, else `START_ALL_WEB_PORT`                                                                       | `start:all`, `local:up`                  |
| Web internal, Nitro behind the gateway | 3001                     | `START_ALL_WEB_INTERNAL_PORT` / `RECEIPT_WEB_INTERNAL_PORT`                                                               | `start:all`, `local:up`                  |
| Integrations provider API              | 3003                     | `START_ALL_INTEGRATIONS_PORT`                                                                                             | `start:all`, `local:up`                  |
| Integrations Connect UI                | 3009                     | `START_ALL_INTEGRATIONS_CONNECT_PORT`                                                                                     | `start:all`, `local:up`                  |
| Slack app                              | 3010                     | `START_ALL_SLACK_PORT`                                                                                                    | `start:all`, `local:up` (optional child) |
| Teams app                              | 3011                     | `START_ALL_TEAMS_PORT`                                                                                                    | `start:all`, `local:up` (optional child) |
| zero-cache                             | 4848                     | `START_ALL_ZERO_CACHE_PORT`, or `ZERO_PORT` under `dev`; `VITE_ZERO_CACHE_URL` is the browser-facing URL and has to match | all three                                |
| zero-cache change-streamer             | 4849, the cache port + 1 | derived, and preflighted                                                                                                  | all three                                |
| Redis                                  | 6380 on the host         | `START_ALL_REDIS_PORT`; the compose file's own default is 6379                                                            | `start:all`, `local:up`                  |
| OpenSandbox controller                 | 8080                     | `START_ALL_OPENSANDBOX_PORT` / `OPEN_SANDBOX_DOMAIN`                                                                      | `start:all`, `local:up`                  |
| Resonate broker                        | 8001                     | `START_ALL_RESONATE_PORT` / `RESONATE_PORT`                                                                               | all three                                |
| Resonate metrics                       | 9090                     | `START_ALL_RESONATE_METRICS_PORT` / `RESONATE_METRICS_PORT`                                                               | all three                                |
| Receipt runtime API                    | 8787                     | `START_ALL_RECEIPT_PORT` / `RECEIPT_PORT`; the runtime falls back to `PORT` only when started on its own                  | all three                                |
| Mock LLM proxy                         | 8789                     | `RECEIPT_MOCK_LLM_PORT`                                                                                                   | `llm:mock` only                          |
| TanStack devtools                      | 42069                    | `TANSTACK_DEVTOOLS_PORT`                                                                                                  | `dev` only                               |
| Factory simulator UI                   | 4397                     | `RECEIPT_SIMULATOR_UI_PORT`                                                                                               | `receipt:simulate:ui` only               |

`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:

```bash theme={null}
START_ALL_WEB_PORT=3100 \
START_ALL_PUBLIC_GATEWAY_URL=http://localhost:3100 \
BETTER_AUTH_URL=http://localhost:3100 \
VITE_BETTER_AUTH_URL=http://localhost:3100 \
./bunw run start:all
```

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.

<Warning>
  **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.
</Warning>

* 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 `exec`s 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`.

<Warning>
  `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.
</Warning>

Next step: [work through what breaks and why](/core/local-development-troubleshooting).
