# 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](./system/deployment.md).

## Accounts you need

| Service                                                | What it gives arbe                                                              | Without it                       |
| ------------------------------------------------------ | -------------------------------------------------------------------------------- | ---------------------------------- |
| [Supabase](https://supabase.com)                       | Postgres, GoTrue auth, PostgREST + RLS, Storage, Vault, one edge function        | Worker throws at startup         |
| [Electric Cloud](https://dashboard.electric-sql.cloud) | Shape sync (Postgres → browser) and durable streams (transcripts) — two products | Same throw                       |
| [Cloudflare](https://cloudflare.com)                   | Workers host for `apps/www` and `apps/docs`                                     | Needs a different SvelteKit adapter |
| [GitHub](https://github.com/settings/developers)       | An OAuth App — the only human sign-in path                                      | No human login; API keys still work |
| [OpenRouter](https://openrouter.ai)                    | Bot replies, the ambient gate, pi's default sandbox model                       | Bots never answer                |

Optional, one feature each:

- [Daytona](https://daytona.io) (`DAYTONA_API_KEY`) — sandboxes. Agents talk but can't run code. [Sprites](https://sprites.dev) is the legacy alternative, selected per environment.
- [Fly.io](https://fly.io) — hosts `apps/workflow-conductor`. Workflows never advance without it; chat is unaffected.
- [ragthis](https://github.com/oskarrough/ragthis) (`RAGTHIS_API_URL` + `RAGTHIS_API_KEY`) — the whole [files](./system/files.md) feature. Nothing to sign up for: it's arbe's own file index and you deploy an instance. Miss either variable and uploads are rejected before Storage, listing falls back to Storage-only, and the file tools are never offered.
- [PostHog](https://posthog.com) — analytics. Keys are public, committed in `apps/www/.env`.
- Direct provider keys (`ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, …) — only for explicit `anthropic/<id>`-style model refs; everything else routes through OpenRouter. See [system/llm-keys](./system/llm-keys.md).
- Cloudflare R2 + GitHub Actions — publishing CLI binaries. Any static host works. See [releases](./system/releases.md).

## Wiring it up

1. Supabase project. Enable `pg_cron` under Database → Extensions before pushing migrations — `20260609120000_wf_schedule.sql` calls `cron.schedule` and fails without it.

   ```sh
   cd packages/supabase && bunx supabase link --project-ref <your-ref>
   cd ../.. && bun run push-migrations     # migrations + regenerated database.types.ts
   ```

   Migrations bring the `house-files` bucket and every RLS policy, so there's no dashboard clicking. Gotchas around `--linked` and the two `supabase/` directories: [system/supabase](./system/supabase.md).

2. GitHub OAuth App, callback `https://<your-ref>.supabase.co/auth/v1/callback`. Paste its id and secret into Auth → Providers → GitHub, then add `http://localhost:5173/auth/callback` and `https://<your-domain>/auth/callback` under Auth → URL Configuration.

3. Electric Cloud. A source over your Supabase Postgres yields `ELECTRIC_SOURCE_ID` and `ELECTRIC_SOURCE_SECRET`. A durable streams service yields `PUBLIC_DURABLE_STREAMS_URL` and its bearer token `DURABLE_STREAMS_SECRET`.

4. Env files. Public values in `apps/www/.env` (committed, baked in at build), secrets in `apps/www/.env.local`. Nine variables are checked at module load and named in the throw if empty — `hooks.server.ts` owns the list. `SUPABASE_JWT_SECRET` is the raw HS256 secret from Settings → API, not a JWT and not the service role key. See [system/environment-variables](./system/environment-variables.md).

5. Run it. `bun install && bun run dev` → `https://localhost:8443`. See [system/development](./system/development.md).

6. Deploy. Your own `account_id` and `routes` in `apps/www/wrangler.jsonc`, then `bunx wrangler secret put <NAME>` per secret. See [system/deployment](./system/deployment.md).

Then, in any order:

7. Sandboxes. `DAYTONA_API_KEY` as worker env or a house secret, plus the one edge function arbe deploys:

   ```sh
   bunx supabase functions deploy stream-proxy
   bunx supabase secrets set ARBE_WORKER_URL=https://<your-domain>/api/stream
   ```

   Daytona's egress allowlist reaches `*.supabase.co` but not Electric, so sandboxes post thread entries through this shim. It forwards method, path, query, body, and the caller's bearer token verbatim to your worker, which owns JWT verification and scope enforcement; it holds no stream secret, which is why `supabase/config.toml` pins `verify_jwt = false`. That file also hardcodes our `project_id` — change it. See [system/sandbox-daytona](./system/sandbox-daytona.md).

8. Files. Deploy ragthis somewhere reachable over HTTP, set its `API_KEY`, point `RAGTHIS_API_URL` and `RAGTHIS_API_KEY` at it. Ingest runs a vision model over images and PDF pages, so budget for that provider too. See [system/files](./system/files.md).

9. Workflows. The conductor on Fly with `DATABASE_URL` (your Supabase pooler URL), `CONDUCTOR_SECRET`, and `APP_URL`. That same secret goes in as a worker secret — it authenticates `x-conductor-secret` on `POST /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. Its ~30s cancellation is why dispatch self-`fetch`es `/api/dispatch/run`; on a host without that cap the indirection is harmless. Moving to `adapter-node` is plausible work, not a rewrite.

Electric is two independent services sharing a vendor. The sync engine is open source and self-hostable. For durable streams, `packages/streams/gateway.ts` is a single-token auth proxy over the embedded `server.ts`, and it's what the package's tests run against. Neither path runs in production here.

The file index is swappable. `packages/core/files.ts` is the only module that talks to ragthis, and no route, tool name, CLI command, or UI string says "rag" or "store". A replacement needs to return the same citation shape — chunk text, file id and filename, page/line coordinates, score.

Fly hosts the conductor, a Bun daemon polling Postgres. Any always-on host works. Daytona has no Fly relationship; the legacy sprite runtime is the Fly one.

Code: `apps/www/src/hooks.server.ts`, `apps/www/wrangler.jsonc`, `packages/supabase/migrations/`, `apps/workflow-conductor/fly.toml`.<br>
See [system/deployment](./system/deployment.md), [system/environment-variables](./system/environment-variables.md), [system/development](./system/development.md), [system/supabase](./system/supabase.md), [architecture](./architecture.md).
