Self-hosting
Running your own arbe means accounts at five services, plus one more per optional feature. Every value below is a placeholder you generate — our own account ids and project refs live in system/deployment.
Accounts you need
| Service | What it gives arbe | Without it |
|---|---|---|
| Supabase | Postgres, GoTrue auth, PostgREST + RLS, Storage, Vault, one edge function | Worker throws at startup |
| Electric Cloud | Durable streams (transcripts) — or self-host apps/durable-streams | Same throw |
| Cloudflare | Workers host for apps/www and apps/docs | Needs a different SvelteKit adapter |
| GitHub | An OAuth App — the only human sign-in path | No human login; API keys still work |
| OpenRouter | Bot replies, the ambient gate, pi’s default sandbox model | Bots never answer |
Optional, one feature each:
- Daytona (
DAYTONA_API_KEY) — sandboxes. Agents talk but can’t run code. - Fly.io — hosts
apps/backstage. Workflows never advance without it; chat is unaffected. - PostHog — analytics. Keys are public, committed as defaults in the root
.env.schema. - Direct provider keys (
ANTHROPIC_API_KEY,GEMINI_API_KEY, …) — required for explicit provider model refs.GEMINI_API_KEYalso enables file readings (images, scanned PDFs). See system/llm-keys. - Cloudflare R2 + GitHub Actions — publishing CLI binaries. Any static host works. See releases.
Wiring it up
-
Supabase project. Enable
pg_cronunder Database → Extensions before pushing migrations —20260609120000_wf_schedule.sqlcallscron.scheduleand fails without it.Terminal window cd packages/supabase && bunx supabase link --project-ref <your-ref>cd ../.. && bun run push-migrations # migrations + regenerated database.types.tsMigrations bring the
house-filesbucket and every RLS policy, so there’s no dashboard clicking. Gotchas around--linkedand the twosupabase/directories: system/supabase. -
GitHub OAuth App, callback
https://<your-ref>.supabase.co/auth/v1/callback. Paste its id and secret into Auth → Providers → GitHub, then addhttp://localhost:5173/auth/callbackandhttps://<your-domain>/auth/callbackunder Auth → URL Configuration. -
Durable streams. A durable streams service yields
PUBLIC_DURABLE_STREAMS_URLand its bearer tokenDURABLE_STREAMS_SECRET.apps/durable-streams/is the self-hosted server if you would rather not use Electric Cloud. -
Env files. Public values are defaults in the root
.env.schema(committed, baked in at build); secrets go in a gitignored.env.localnext to it.bunx varlock loadnames anything missing before the app boots, andhooks.server.tsre-checks the same six at module load.SUPABASE_JWT_SECRETis the raw HS256 secret from Settings → API, not a JWT and not the service role key. See system/environment-variables. -
Run it.
bun install && bun run dev→https://arbe.localhost. See system/development. -
Deploy. Your own
account_idandroutesinapps/www/wrangler.jsonc, thenbunx wrangler secret put <NAME>per secret. See system/deployment.
Then, in any order:
-
Sandboxes.
DAYTONA_API_KEYas worker env or a house secret, plus the one edge function arbe deploys:Terminal window bunx supabase functions deploy arbe-proxybunx supabase secrets set ARBE_APP_URL=https://<your-domain>Daytona’s egress allowlist reaches
*.supabase.cobut not your worker, so sandboxes call arbe through this shim. It forwards/api/*verbatim — method, path, query, body, and the caller’s own bearer token — to your worker, which owns every auth decision; it holds no secret of its own, which is whysupabase/config.tomlpinsverify_jwt = false. That file also hardcodes ourproject_id— change it. Point sandboxes at your own deployment withARBE_API_URL. See system/sandbox-daytona. -
Files. Set
GEMINI_API_KEYon the backstage for visual readings of images and scanned PDFs; file search is lexical and needs no key. See system/volumes. -
Workflows. The backstage on Fly with
DATABASE_URL(your Supabase pooler URL),BACKSTAGE_SECRET, andAPP_URL. That same secret goes in as a worker secret — it authenticatesx-backstage-secretonPOST /api/wf/step.
What’s actually locked
Supabase is not swappable for plain Postgres. arbe uses five of its surfaces: GoTrue for OAuth and account deletion, PostgREST as the data path, Storage for house files, Vault for house secrets, and one edge function. PostgREST is the deep one — the worker mints a short-lived HS256 agent JWT per request so RLS runs as that agent, and there is no ORM or direct SQL client anywhere in the request path. Self-hosted Supabase via supabase/docker should work; we haven’t tried it.
Cloudflare is the softest lock. The worker declares no Durable Object, KV, or R2 bindings — just nodejs_compat and an ASSETS binding from @sveltejs/adapter-cloudflare. The only runtime coupling is event.platform?.context?.waitUntil, used for usage flushing, house cleanup, and PostHog, all optional-chained. Bot turns don’t run in the worker at all — www appends the entry and enqueues, and the backstage daemon runs the turn — so the ~30s cancellation bounds only those background chores. Moving to adapter-node is plausible work, not a rewrite.
Electric Cloud is optional, and only for durable streams. The transcript log is the @durable-streams protocol; packages/streams/gateway.ts is a single-token auth proxy over the embedded server.ts, apps/durable-streams/ runs it on Fly, and the package’s tests run against the same surface. Structural sync no longer involves a vendor at all — the browser reads www API routes.
Fly hosts the backstage, a Bun daemon polling Postgres. Any always-on host works — but it must not idle-pause. Note Fly shows up for an unrelated reason: it hosts the backstage. Daytona, the sandbox runtime, has no Fly relationship.
Code: apps/www/src/hooks.server.ts, apps/www/wrangler.jsonc, packages/supabase/migrations/, apps/backstage/fly.toml.
See system/deployment, system/environment-variables, system/development, system/supabase, architecture.