Self-hosting
Running your own arbe means accounts at four 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 |
| Fly.io | Hosts apps/durable-streams (transcripts) and apps/backstage (workflows) | Worker throws; workflows stall |
| Cloudflare | Workers host for apps/www and apps/docs | Needs a different SvelteKit adapter |
| OpenRouter | Bot replies, the ambient gate, pi’s default sandbox model | Bots never answer |
Optional, one feature each:
- GitHub — an OAuth App for “Sign in with GitHub”. Email link and password sign-in work without it.
- Daytona (
DAYTONA_API_KEY) — sandboxes. Agents talk but can’t run code. - 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. -
Sign-in. Email link and email + password use Supabase’s built-in email auth; add
https://arbe.localhost/auth/callbackandhttps://<your-domain>/auth/callbackunder Auth → URL Configuration so the links land back in arbe. Supabase’s default mailer is heavily rate-limited, so set custom SMTP before real users arrive. For GitHub sign-in, create an OAuth App with callbackhttps://<your-ref>.supabase.co/auth/v1/callbackand paste its id and secret into Auth → Providers → GitHub. -
Durable streams. Deploy
apps/durable-streams/to Fly — the upstream durable-streams server with a Fly volume and one bearer token. Its URL becomesPUBLIC_DURABLE_STREAMS_URLand the tokenDURABLE_STREAMS_SECRET; www and the backstage must point at the same one. Setup steps are in its README. -
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. Varlock validates the app schema before startup; www and backstage read its typedENVobject.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. Set your own
account_idandroutesinapps/www/wrangler.jsoncand supply secrets in Cloudflare Builds. The deploy script usesvarlock-wranglerto validate and upload them. 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 sign-in 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.
Durable streams are an open protocol, not a vendor. apps/durable-streams/ runs the upstream server on Fly, and packages/streams/gateway.ts is the same surface embedded for local dev and tests; any host that keeps a disk and long-lived connections works. Structural sync involves no sync service at all — the browser reads www API routes.
Fly also hosts the backstage, a Bun daemon polling Postgres. Any always-on host works for either — but neither may idle-pause. 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, apps/durable-streams/fly.toml.
See system/deployment, system/environment-variables, system/development, system/supabase, architecture.