Skip to main content
Once the stack is up, almost every local problem is one of four things: you changed code and nothing picked it up, there is no model credential where the code looks for one, you need to work without a provider account, or you hit a failure that already has a known fix. This page takes them in that order. For prerequisites, bring-up, the run modes and the port map, start at Local development.

Nothing in local:up hot-reloads

The web tier is a prebuilt production bundle with no Vite process, and every server process on that path is spawned without --watchRECEIPT_SERVER_WATCH is never set here — so each holds its module graph for its process lifetime. Plan on an explicit action per change.
For an apps/start UI edit loop, local:up is the wrong tool — every change costs a full production rebuild. Use ./bunw run dev, which gives you Vite HMR and runs the runtime’s api role under --watch, at the cost of a partial stack. The two cannot run at the same time; they want the same ports.
If you use LOCAL_UP_BUILD_WEB=0 to reuse an existing bundle, build by hand inside apps/start:
Prefixing the root build does not work: it routes through Turborepo, whose strict env mode drops any variable absent from its global list, and you get a bundle that demands an email code you cannot receive locally. local:up sets it for you, so the supported path never hits this.

Model credentials come from the database, not the environment

Model credentials are organization-scoped rows in Postgres, not a process variable. Funding resolves in this order: a Slack-originated billing request id beginning slack_evt_ may authorize platform credit first; otherwise an encrypted key for the organization or workspace, decrypted with BYOK_ENCRYPTION_KEY_B64 — this is the normal path; and only with a billing request id, the process-wide OPENAI_API_KEY as platform credit. So setting OPENAI_API_KEY in your environment file alone will not make agent runs work. Sign up locally, create an organization, and paste a provider key into its model settings. receipt doctor reflects this deliberately: it prints the fixed line provider auth: organization BYOK required and performs no model-key check at all. With neither a stored key nor platform credit you will see, in web chat:
  • Platform-funded OpenAI access is unavailable: OPENAI_API_KEY is not configured.
  • Platform-funded OpenAI embeddings are unavailable: OPENAI_API_KEY is not configured.
And from the runtime’s funding resolver, when credit is authorized but no key is present: Platform-funded OpenAI access is authorized, but OPENAI_API_KEY is unavailable.
Do not rotate BYOK_ENCRYPTION_KEY_B64 or RECEIPT_CONNECTION_ENCRYPTION_KEY_B64 once anything has been saved with them, not even locally — the rotation cannot be undone, and Configuration, secrets, and keys explains what it destroys. Generate both once, before you sign up for the first time.

Offline development with the mock LLM proxy

To develop without a provider account, run the repository’s mock model server:
It is a dependency-free Node HTTP server implementing the subset of the OpenAI API the runtime uses. On boot it prints exactly:
For a JSON Schema it synthesizes a value by walking constenumanyOf/oneOf → object, array, boolean, number, null, picking strings by keyword (thought, summary, title, reason, text/answer, input). Two schema names get hand-written shapes: FactoryChatTurnAnalysis and agent_action. Point the runtime at it:
The mock-key fallback returns MOCK_OPENAI_API_KEY, or the literal mock-openai-key when that is unset — but only when the hostname in OPENAI_BASE_URL is localhost, 127.0.0.1 or ::1. The code states why: keeping the fallback loopback-only stops production credential resolution from silently degrading into process-wide credentials. When the mock key is in use, the key reader short-circuits Postgres and logs a line carrying "source":"local_mock_openai_proxy".
The proxy’s own banner says MOCK_OPENAI_API_KEY=mock-key while the code’s default is mock-openai-key. The value does not matter — the proxy ignores the bearer token entirely.

Symptom, cause, fix

Toolchain and bring-up

Ports, and processes that outlive shutdown

Sync and the database

Sync and migration failures behave the same locally as on a deployment, so they are catalogued once, with their causes, in Postgres, migrations, and the sync publication — a UI that loads with no data, a newly published table with no rows, zero-cache exiting shortly after a schema reset, and relation "<schema>.receipt_job_projection" does not exist during a migration. The checksum refusal you get after editing an already-applied migration is on the same page, under never edit an applied migration. Locally the blunt fix for the first three is ./bunw run --cwd apps/start zero:reset, which recreates the zero_data publication and deletes zero.db* so the replica is rebuilt from scratch; clear browser site data afterwards, and confirm Postgres is running with wal_level=logical.

Environment values and origins

Model access, sandboxes and integrations

The supervisor itself

Two commands look like cleanup and are not: docker compose -f docker-compose.postgres.yml down -v deletes the database volume, and docker system prune -a deletes the local OpenSandbox worker image. Neither belongs in a routine stop — see stopping the stack.
Next step: run Receipt on your own infrastructure.