Self-hosting
Install & deploy
Run the whole platform on your own infrastructure. You need a Supabase project for the database and auth, and either Docker or Node.
One-command setup
The setup script scaffolds .env, generates the encryption secrets, applies database migrations and starts the stack.
./scripts/setup.sh --allpowershell -ExecutionPolicy Bypass -File scripts\setup.ps1 -All--all brings up every service and is the right default for a full install. Without it you get the app alone: add --docgen for the server-side Office renderer, --notebooks for the Developer-workspace Python runtime, or --sandbox for custom code in deployed swarms. --dev runs a local dev server instead of containers.
It cannot create your Supabase project or guess its keys — it writes the .env, tells you which values to fill in, and you re-run it.
Manual setup
- 1
Create a Supabase project
It provides Postgres, authentication and storage. Note the project URL, publishable key and service-role key. - 2
Fill in .env
Copy.env.exampleand set the required values below. - 3
Apply migrations
npx supabase link --project-ref <ref>thennpx supabase db push. This creates every table, policy and storage bucket. - 4
Start it
docker compose up -d --build, ornpm install && npm run dev. Openhttp://localhost:8080.
Migrations are not optional
npx supabase db push before concluding a feature is broken.Environment reference
Every variable the app reads, grouped by what it does. Only the first group is required; everything else changes behaviour you may not need.
Required — Supabase and identity
| Variable | Purpose |
|---|---|
SUPABASE_URL | Project URL, server side |
SUPABASE_PUBLISHABLE_KEY | Anon key, server side |
SUPABASE_SERVICE_ROLE_KEY | Service role. Server only — must never reach a browser. |
VITE_SUPABASE_URL | Same URL, inlined into the client bundle |
VITE_SUPABASE_PUBLISHABLE_KEY | Same anon key, client side |
ADMIN_EMAIL | Bootstrap superadmin account |
VITE_ADMIN_EMAIL | Same address, for client-side admin affordances |
PROVIDER_CREDS_SECRET | Encryption key for every stored credential. Back this up — see the warning below. |
INTERNAL_RUN_SECRET | Signs internal service-to-service calls |
PROVIDER_CREDS_SECRET is not recoverable
Models and search
| Variable | Purpose |
|---|---|
OPENROUTER_API_KEY | Zero-config model fallback so a fresh workspace works before anyone connects their own provider. |
OPENROUTER_DEFAULT_MODEL | Model used for that fallback |
OPENROUTER_BASE_URL | Point at a compatible gateway instead of OpenRouter |
OPENAI_API_KEY | Workspace-wide OpenAI key |
FIRECRAWL_API_KEY | Workspace-wide web search and page fetching for the web_search / web_browse tools |
Data connections
None of these are required — the defaults are the recommended settings. They exist for tuning against a particular warehouse or network.
| Variable | Default | Purpose |
|---|---|---|
WAREHOUSE_MAX_ROWS | 1000 | Rows returned when a caller asks for none |
WAREHOUSE_ABS_MAX_ROWS | 5000 | Hard ceiling no caller can exceed |
WAREHOUSE_QUERY_TIMEOUT_MS | 60000 | Wall-clock budget for one query |
WAREHOUSE_MAX_CONCURRENT | 8 | Queries in flight, per instance |
WAREHOUSE_MAX_CONCURRENT_PER_USER | 3 | Per tenant |
WAREHOUSE_POOL | on | Connection pooling for PostgreSQL/MySQL-family sources. Set off for a connection per query. |
WAREHOUSE_POOL_MAX | 4 | Sockets per credential set — see the sizing note below |
WAREHOUSE_POOL_MAX_KEYS | 64 | Distinct credential sets held at once |
HTTPS_PROXY | — | Forward proxy for all outbound connector traffic. NO_PROXY takes a bypass list. |
CONNECTOR_MAX_RETRIES | 2 | Retries on 429/503 and transport errors |
CONNECTOR_RETRY_500 | off | Also retry 500s — only for providers that use 500 for throttling |
CONNECTION_HEALTH_HOURS | 12 | Credential re-validation cadence |
CREDENTIAL_MAX_AGE_DAYS | 90 | When a credential is badged as old |
Size the pool against your database, not this page
WAREHOUSE_POOL_MAX × WAREHOUSE_POOL_MAX_KEYS × your replica count, and each replica keeps its own pools. Check that product against the database’s max_connections before raising either number. Pooling is worth having — it took a query from 30.7ms to 2.9ms in measurement — but an oversized pool exhausts a warehouse’s connection limit instead.Email delivery
Carries welcome mail, budget alerts, BI alerts, scheduled reports, approval requests and the contact form. Use either Resend or SMTP. Auth emails (confirmation, password reset) are separate — Supabase sends those, configured in its own dashboard.
Resend needs a verified domain, not just a key. In Resend: create an API key for RESEND_API_KEY, then Domains → Add Domain, publish the SPF and DKIM records it gives you at your DNS host, and press Verify. Then set EMAIL_FROM to an address on that domain.
RESEND_API_KEY="re_..."
EMAIL_FROM="AgentSwarms <noreply@your-company.com>"
SITE_URL="https://your-domain.com"Two ways email fails without looking broken
EMAIL_FROM empty falls back to noreply@example.com, which Resend rejects — every app email fails while the app carries on normally. And until your domain is verified, Resend sends only from onboarding@resend.dev and delivers only to the address that owns the Resend account; mail to anyone else is accepted by the API and never arrives. Both outcomes are recorded in email_send_log, which is where to look when nobody is receiving anything.| Variable | Purpose |
|---|---|
RESEND_API_KEY | Resend delivery |
SMTP_HOST | SMTP delivery |
SMTP_PORT | — |
SMTP_USER | — |
SMTP_PASS | — |
SMTP_SECURE | TLS on/off |
EMAIL_FROM | From address on outgoing mail |
SITE_URL | Base URL used in links inside emails |
PUBLIC_APP_URL | Public base URL of this instance |
Run limits and cost
| Variable | Purpose |
|---|---|
SWARM_RUN_RATE_LIMIT_PER_MIN | Requests per API key per minute, then 429 |
SWARM_RUN_MAX_CONCURRENT | Simultaneous runs per key |
SWARM_RUN_TIMEOUT_MS | Wall-clock ceiling for one run |
ENFORCE_BUDGET_CAP | Makes budget caps BLOCK rather than only alert. Accepts 1, true, yes. Set this on any instance with a public embed — see Budgets. |
BUDGET_FAIL_CLOSED | What to do when the spend lookup itself fails, as opposed to coming back under cap. Unset (the default) allows the call — governance should not be the reason legitimate work breaks. Set true when the cap must hold even if the figure cannot be established. Either way the failure is logged. |
Note
Network egress
| Variable | Purpose |
|---|---|
BLOCK_PRIVATE_NETWORK_FETCH | Refuse outbound requests to private, loopback and link-local addresses, including cloud metadata endpoints. |
ALLOW_PRIVATE_NETWORK_FETCH | The escape hatch, for when a warehouse or MCP server genuinely lives on a private network. |
TRUSTED_PROXY_HOPS | How many reverse proxies of yours sit in front of the app. Decides which entry of X-Forwarded-For is treated as the caller — the header is appended to, so only the entries your own proxies added cannot be forged. Default 1 (a single reverse proxy); use 2 behind a CDN in front of that proxy. Only MCP key IP allow-lists depend on it. |
Careful
TRUSTED_PROXY_HOPS to match your actual topology before relying on an MCP key's IP allow-list. Too low reads your proxy's address instead of the caller's and the allow-list never matches; too high reads a value the caller supplied, which is the bypass the setting exists to close. It is clamped to the length of the chain, so it can never walk past the end.Careful
web_browse, a swarm HTTP node, or a prompt-injected instruction — can reach inside your network. If you must enable it, do so on an instance with no public embeds.Observability and audit
| Variable | Purpose |
|---|---|
METRICS_TOKEN | Bearer token guarding the metrics endpoint |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP collector endpoint |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | Traces-specific override |
OTEL_EXPORTER_OTLP_HEADERS | Extra headers for the collector |
OTEL_SERVICE_NAME | Service name reported in traces |
AUDIT_ARCHIVE_ON_PURGE | Archive audit events instead of dropping them at retention |
PERSIST_PROMPT_BODIES | Whether full prompt and response bodies are stored on traces. Rich for debugging, heavier and more sensitive — decide deliberately. |
Scheduling
| Variable | Purpose |
|---|---|
DISABLE_INPROCESS_SCHEDULER | Turn off the in-process scheduler on the web tier — see scaling below. |
BI_CRON_TOKEN | Token an external cron presents to the BI cron endpoint |
NOTEBOOK_CRON_TOKEN | Same, for the notebook reaper |
Document renderer
| Variable | Purpose |
|---|---|
DOCGEN_SERVICE_URL | Only when the renderer runs somewhere unusual. Leave empty — the app probes docgen:8099 and localhost:8099 and uses whichever answers. |
DOCGEN_TOKEN | Shared bearer token between the app and the renderer |
Notebook runtime
| Variable | Purpose |
|---|---|
NOTEBOOK_RUNTIME_ENABLED | Turn the server runtime on |
NOTEBOOK_RUNTIME_SECRET | Session-token signing key. Omit and the server generates one. |
NOTEBOOK_RUNTIME_BACKEND | docker | k8s | e2b |
NOTEBOOK_RUNTIME_IMAGE | Kernel image to launch |
NOTEBOOK_GATEWAY_URL | Websocket gateway address |
Configuration by use case
The reference above lists every setting. These are the combinations that actually go together, as complete blocks you can paste into .env. Each one names the risk it is answering, because the defaults are chosen for a single trusted operator and stop being right as soon as anyone else can reach the instance.
Evaluating it on a laptop
Nothing is exposed, so nothing needs hardening. This is the default and you can ignore every other recipe until someone else can reach the app.
SUPABASE_URL="https://<project>.supabase.co"
SUPABASE_SERVICE_ROLE_KEY="..."
SUPABASE_PUBLISHABLE_KEY="..."
VITE_SUPABASE_URL="https://<project>.supabase.co"
VITE_SUPABASE_PUBLISHABLE_KEY="..."
# One key and you can use everything; per-user keys can come later.
OPENROUTER_API_KEY="sk-or-..."
ADMIN_EMAIL="you@example.com"
VITE_ADMIN_EMAIL="you@example.com"An internal tool for one team
Reachable on your network or a private domain, no anonymous visitors. The work here is closing signup and making sure a runaway agent cannot bill you indefinitely.
SITE_URL="https://agents.internal.example.com"
PUBLIC_APP_URL="https://agents.internal.example.com"
# Encrypts stored warehouse/SaaS credentials. Set once — rotating it
# invalidates everything already saved. openssl rand -hex 32
PROVIDER_CREDS_SECRET="..."
# Caps stop being advisory.
ENFORCE_BUDGET_CAP="true"
# One reverse proxy in front (Caddy/nginx).
TRUSTED_PROXY_HOPS="1"
# Self-hosted Ollama or an in-cluster MCP server lives on a private address,
# so leave private-network fetches allowed — cloud metadata stays blocked
# either way.Note
Public embeds on a marketing site
The hardest case, because anonymous visitors spend your credits and you cannot authenticate them. Every setting here bounds what a stranger — or a leaked embed key — can cost you.
SITE_URL="https://www.example.com"
PUBLIC_APP_URL="https://app.example.com"
PROVIDER_CREDS_SECRET="..."
# Refuse calls past the cap instead of emailing about them afterwards.
ENFORCE_BUDGET_CAP="true"
# If spend cannot be established, refuse rather than assume zero.
BUDGET_FAIL_CLOSED="true"
# A public embed has no reason to reach anything inside your network.
BLOCK_PRIVATE_NETWORK_FETCH="true"
TRUSTED_PROXY_HOPS="1"
# Tighten the public surface below the defaults (30/min chat, 10/min ask).
MCP_RATE_LIMIT_PER_MIN="30"
SWARM_RUN_RATE_LIMIT_PER_MIN="10"
SWARM_RUN_MAX_CONCURRENT="2"Careful
Regulated or air-gapped
No outbound anything, evidence retained, and the audit trail shipped somewhere the app cannot rewrite.
BLOCK_PRIVATE_NETWORK_FETCH="true"
ENFORCE_BUDGET_CAP="true"
BUDGET_FAIL_CLOSED="true"
# Traces to your own collector; nothing leaves for a vendor.
OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector.internal:4318"
OTEL_SERVICE_NAME="agentswarms-prod"
# Expiring audit rows are printed as NDJSON before deletion, so a log
# shipper keeps them past the database's own retention.
AUDIT_ARCHIVE_ON_PURGE="1"
# No model prices are fetched at runtime; the table is vendored in the repo
# and refreshed deliberately with: npm run prices:refreshNote
Autoscaled behind a load balancer
Several app instances against one Supabase project. Rate limits and concurrency slots are counted in Postgres, so the numbers you set are the numbers you get — but two settings need to match the topology.
# MUST be set: instances resolve their own origin from this, never from the
# request's Host header.
PUBLIC_APP_URL="https://app.example.com"
# A dedicated secret for server-to-server calls, so the database master key
# stays out of outbound headers. openssl rand -hex 32
INTERNAL_RUN_SECRET="..."
# CDN in front of the load balancer? Then two hops, not one.
TRUSTED_PROXY_HOPS="2"
# Alerts, refreshes and purges run in-process. A cross-instance lease stops
# them double-firing, but the tidier arrangement on a fleet is to disable
# them on the web tier and drive /api/bi/cron from one external scheduler.
DISABLE_INPROCESS_SCHEDULER="true"
BI_CRON_TOKEN="..."Optional services
| Service | Profile | What it adds |
|---|---|---|
| Doc-gen renderer | --profile docgen | Server-side PowerPoint/Word/Excel via python-pptx, python-docx, openpyxl and LibreOffice — the "Deep" generation mode. |
| Notebook runtime | --profile notebooks | Real Python kernels for the Developer workspace, with a gateway and a default-deny egress proxy. |
| JS sandbox | --profile sandbox | Runs Function nodes and custom components in deployed and scheduled swarms, in a locked-down container instead of next to the app's credentials. |
docker compose --profile docgen --profile notebooks --profile sandbox up -d --buildOr let the setup script start everything: bash scripts/setup.sh --all (powershell -File scripts\setup.ps1 -All on Windows).
All three are optional, and each degrades to something rather than breaking. Without the renderer, documents are generated in the browser and Deep mode is greyed out with the reason. Without the notebook runtime, notebooks fall back to the in-browser Python runtime. Without the sandbox, custom code still runs on the canvas and the Deploy dialog says plainly that it will fail in headless runs.
Observability → Monitoring (superadmin) shows which of these are actually up on this deployment, with the address that answered and live CPU, memory and disk. A profile you chose not to start reads “Not running” rather than as a failure.
Deployment targets
- Docker Compose
- The default. One app container plus whichever optional profiles you enable. Good to a substantial team on one host.
- Node behind a reverse proxy
- Build and run the server directly. Terminate TLS at your proxy.
- Autoscaled VMs behind a load balancer
- The app tier is stateless, so run as many identical containers as you need. Set DISABLE_INPROCESS_SCHEDULER=1 and drive background work from one external cron.
- Kubernetes
- Manifests are provided for the app and the notebook runtime, including the egress policy that keeps kernels off the open internet.
Scaling
The app tier is stateless — no sticky sessions needed, so put as many instances behind a load balancer as you like. Two things need attention when you do:
- The scheduler. Alerts, refreshes and purges run in-process. A cross-instance lease prevents double-firing, but the tidier arrangement is
DISABLE_INPROCESS_SCHEDULERon the web tier and one external cron hitting the cron endpoint. - Limits hold across the fleet. Rate limits and concurrency slots are counted in Postgres, so the number you configure is the number you get however many instances are running. If the database is briefly unreachable an instance falls back to counting locally and logs that it has — the limit weakens rather than vanishing. Budget caps are the other ceiling, and they are counted the same way; see Budgets.
- Set the proxy depth.
TRUSTED_PROXY_HOPSmust match how many proxies of yours sit in front —1for a load balancer alone,2with a CDN in front of it. MCP key IP allow-lists are checked against the address it selects.
The notebook Docker runtime is single-host by design — it launches containers on the host it runs on. Use the Kubernetes orchestrator to spread it.
Operations
- Health
- A health endpoint reports process liveness — point your load balancer at it.
- Backups
- Supabase holds all durable state. Use its backups, and store PROVIDER_CREDS_SECRET separately — a database backup without it is unreadable for credentials.
- Upgrades
- Pull, rebuild, then push migrations. Migrations are additive; check the release notes before skipping several versions.
- Logs
- Container logs for the platform; in-app Traces for what agents did. They answer different questions — reach for Traces first when an agent misbehaves.
Before you expose it
- Turn off public signup, or enforce SSO — Access control.
- Set
ENFORCE_BUDGET_CAPand give every embed and API key a cap. - Serve over TLS; the service-role key must never reach a browser.
- Restrict embed keys to your own domains.
- Review retention windows for chats, transcripts and audit.
- Back up
PROVIDER_CREDS_SECRETsomewhere you can actually retrieve it.
Note
docs/INSTALL.md in the repository, which is kept up to date as issues are found.