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.

bash
./scripts/setup.sh --all
powershell
powershell -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. 1

    Create a Supabase project

    It provides Postgres, authentication and storage. Note the project URL, publishable key and service-role key.
  2. 2

    Fill in .env

    Copy .env.example and set the required values below.
  3. 3

    Apply migrations

    npx supabase link --project-ref <ref> then npx supabase db push. This creates every table, policy and storage bucket.
  4. 4

    Start it

    docker compose up -d --build, or npm install && npm run dev. Open http://localhost:8080.

Migrations are not optional

Features whose migrations haven't been applied fail quietly rather than loudly — a storage bucket that doesn't exist means uploads silently don't persist, and a missing column means a setting has nowhere to save. After any upgrade, run 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

VariablePurpose
SUPABASE_URLProject URL, server side
SUPABASE_PUBLISHABLE_KEYAnon key, server side
SUPABASE_SERVICE_ROLE_KEYService role. Server only — must never reach a browser.
VITE_SUPABASE_URLSame URL, inlined into the client bundle
VITE_SUPABASE_PUBLISHABLE_KEYSame anon key, client side
ADMIN_EMAILBootstrap superadmin account
VITE_ADMIN_EMAILSame address, for client-side admin affordances
PROVIDER_CREDS_SECRETEncryption key for every stored credential. Back this up — see the warning below.
INTERNAL_RUN_SECRETSigns internal service-to-service calls

PROVIDER_CREDS_SECRET is not recoverable

Every stored credential is encrypted with it, and it lives in the environment rather than the database — so a database dump alone yields no secrets. Lose it and every connector, provider key and MCP token must be re-entered. Keep it wherever you keep your other break-glass secrets, and back it up separately from the database.

Models and search

VariablePurpose
OPENROUTER_API_KEYZero-config model fallback so a fresh workspace works before anyone connects their own provider.
OPENROUTER_DEFAULT_MODELModel used for that fallback
OPENROUTER_BASE_URLPoint at a compatible gateway instead of OpenRouter
OPENAI_API_KEYWorkspace-wide OpenAI key
FIRECRAWL_API_KEYWorkspace-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.

VariableDefaultPurpose
WAREHOUSE_MAX_ROWS1000Rows returned when a caller asks for none
WAREHOUSE_ABS_MAX_ROWS5000Hard ceiling no caller can exceed
WAREHOUSE_QUERY_TIMEOUT_MS60000Wall-clock budget for one query
WAREHOUSE_MAX_CONCURRENT8Queries in flight, per instance
WAREHOUSE_MAX_CONCURRENT_PER_USER3Per tenant
WAREHOUSE_POOLonConnection pooling for PostgreSQL/MySQL-family sources. Set off for a connection per query.
WAREHOUSE_POOL_MAX4Sockets per credential set — see the sizing note below
WAREHOUSE_POOL_MAX_KEYS64Distinct credential sets held at once
HTTPS_PROXYForward proxy for all outbound connector traffic. NO_PROXY takes a bypass list.
CONNECTOR_MAX_RETRIES2Retries on 429/503 and transport errors
CONNECTOR_RETRY_500offAlso retry 500s — only for providers that use 500 for throttling
CONNECTION_HEALTH_HOURS12Credential re-validation cadence
CREDENTIAL_MAX_AGE_DAYS90When a credential is badged as old

Size the pool against your database, not this page

Sockets held is roughly 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.

bash
RESEND_API_KEY="re_..."
EMAIL_FROM="AgentSwarms <noreply@your-company.com>"
SITE_URL="https://your-domain.com"

Two ways email fails without looking broken

Leaving 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.
VariablePurpose
RESEND_API_KEYResend delivery
SMTP_HOSTSMTP delivery
SMTP_PORT
SMTP_USER
SMTP_PASS
SMTP_SECURETLS on/off
EMAIL_FROMFrom address on outgoing mail
SITE_URLBase URL used in links inside emails
PUBLIC_APP_URLPublic base URL of this instance

Run limits and cost

VariablePurpose
SWARM_RUN_RATE_LIMIT_PER_MINRequests per API key per minute, then 429
SWARM_RUN_MAX_CONCURRENTSimultaneous runs per key
SWARM_RUN_TIMEOUT_MSWall-clock ceiling for one run
ENFORCE_BUDGET_CAPMakes budget caps BLOCK rather than only alert. Accepts 1, true, yes. Set this on any instance with a public embed — see Budgets.
BUDGET_FAIL_CLOSEDWhat 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

These limits are counted in Postgres, shared by every instance, so the number you set is the number you get however many copies of the app are running. If the database is briefly unreachable each instance falls back to counting locally and logs that it has done so — the limit degrades rather than disappearing.

Network egress

VariablePurpose
BLOCK_PRIVATE_NETWORK_FETCHRefuse outbound requests to private, loopback and link-local addresses, including cloud metadata endpoints.
ALLOW_PRIVATE_NETWORK_FETCHThe escape hatch, for when a warehouse or MCP server genuinely lives on a private network.
TRUSTED_PROXY_HOPSHow 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

Set 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

Allowing private-network fetches means a URL chosen by a model — from 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

VariablePurpose
METRICS_TOKENBearer token guarding the metrics endpoint
OTEL_EXPORTER_OTLP_ENDPOINTOTLP collector endpoint
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTTraces-specific override
OTEL_EXPORTER_OTLP_HEADERSExtra headers for the collector
OTEL_SERVICE_NAMEService name reported in traces
AUDIT_ARCHIVE_ON_PURGEArchive audit events instead of dropping them at retention
PERSIST_PROMPT_BODIESWhether full prompt and response bodies are stored on traces. Rich for debugging, heavier and more sensitive — decide deliberately.

Scheduling

VariablePurpose
DISABLE_INPROCESS_SCHEDULERTurn off the in-process scheduler on the web tier — see scaling below.
BI_CRON_TOKENToken an external cron presents to the BI cron endpoint
NOTEBOOK_CRON_TOKENSame, for the notebook reaper

Document renderer

VariablePurpose
DOCGEN_SERVICE_URLOnly when the renderer runs somewhere unusual. Leave empty — the app probes docgen:8099 and localhost:8099 and uses whichever answers.
DOCGEN_TOKENShared bearer token between the app and the renderer

Notebook runtime

VariablePurpose
NOTEBOOK_RUNTIME_ENABLEDTurn the server runtime on
NOTEBOOK_RUNTIME_SECRETSession-token signing key. Omit and the server generates one.
NOTEBOOK_RUNTIME_BACKENDdocker | k8s | e2b
NOTEBOOK_RUNTIME_IMAGEKernel image to launch
NOTEBOOK_GATEWAY_URLWebsocket 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.

bash
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.

bash
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

Then turn on invite-only under Admin → IAM so the login page stops accepting new signups. See Access control.

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.

bash
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

Give every embed key and swarm API key its own cap under Budgets. The per-user cap is not enough on its own: it is what a leaked key drains, and a per-credential cap is what stops it.

Regulated or air-gapped

No outbound anything, evidence retained, and the audit trail shipped somewhere the app cannot rewrite.

bash
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:refresh

Note

Audit retention is set in the product, not the environment — Admin → IAM, default 365 days. Deleting a user no longer deletes their trail: the row is kept with the actor's email so an investigation still has something to read.

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.

bash
# 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

ServiceProfileWhat it adds
Doc-gen renderer--profile docgenServer-side PowerPoint/Word/Excel via python-pptx, python-docx, openpyxl and LibreOffice — the "Deep" generation mode.
Notebook runtime--profile notebooksReal Python kernels for the Developer workspace, with a gateway and a default-deny egress proxy.
JS sandbox--profile sandboxRuns Function nodes and custom components in deployed and scheduled swarms, in a locked-down container instead of next to the app's credentials.
bash
docker compose --profile docgen --profile notebooks --profile sandbox up -d --build

Or 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_SCHEDULER on 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_HOPS must match how many proxies of yours sit in front — 1 for a load balancer alone, 2 with 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_CAP and 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_SECRET somewhere you can actually retrieve it.

Note

Install problems and their fixes are collected in docs/INSTALL.md in the repository, which is kept up to date as issues are found.