DocsSelf-hosting
Start here
Build and run
DocsBuild and run
Self-hosting
One container against a PostgreSQL 17 database you own. It applies its own schema on boot and needs no vault, no cache and no object store.
Prerequisites
- Docker with the Compose plugin, v2 or newer.
- PostgreSQL 17. The compose file brings up its own. Point
DATABASE_URLelsewhere if you already run one. Older majors are not tested against this schema. - Node 22.13 or newer, only if you intend to run from source rather than from the image.
- A public HTTPS origin, if MCP clients will connect from anywhere other than the machine the gateway runs on. Terminate TLS in a reverse proxy in front of the container.
Quickstart
Take the self-hosting compose file, generate the two secrets, write a .env beside it, and bring it up.
curl -fsSL https://toolspoke.com/self-host/compose.yaml -o compose.self-host.yamlecho "BETTER_AUTH_SECRET=$(openssl rand -base64 32)"
echo "CREDENTIALS_ENCRYPTION_KEY=$(openssl rand -base64 32)"POSTGRES_PASSWORD=a-password-you-choose
BETTER_AUTH_SECRET=...
CREDENTIALS_ENCRYPTION_KEY=...
APP_URL=https://toolspoke.example.comdocker compose -f compose.self-host.yaml up -dOpen your origin and create an account. The first account creates the workspace around it, with a Default project inside and 5,000 credits on the Free plan. The signed-in product lives under /app.
The environment it requires
| Variable | What it is |
|---|---|
DATABASE_URL | The PostgreSQL connection string. The application refuses to start without it, rather than failing later and further from the cause. |
BETTER_AUTH_SECRET | Signs sessions and the OAuth access tokens the gateway verifies. Rotating it signs everyone out. |
CREDENTIALS_ENCRYPTION_KEY | Encrypts every stored credential with AES-256-GCM before it is written. Minimum 32 characters, and deliberately a different value from the auth secret so that rotating one does not destroy the other's data. |
NEXT_PUBLIC_APP_URL | The public origin, with no trailing slash. Canonical URLs, the address MCP clients are handed, and the audience stamped into OAuth tokens all derive from it. |
Set BETTER_AUTH_URL to the same origin as NEXT_PUBLIC_APP_URL. The compose file sets both from one variable so they cannot drift: a token minted against one origin and verified against another is rejected as the wrong audience.
Everything else is optional, and the feature it configures is unavailable when it is absent. Stripe gives you checkout, and without it every workspace stays on the Free plan. SMTP gives you outbound mail, and without it invitations are still created and their link is copied by hand. An OpenRouter key gives you the toolkit builder and the semantic pass on tool search. Each one is documented inline in the compose file, next to the service that reads it.
Migrations
There is no migration step. The container's entrypoint applies the schema before the server starts listening. The schema is idempotent and takes an advisory lock, so several replicas booting at once is safe and an existing volume is upgraded in place rather than recreated.
docker compose -f compose.self-host.yaml up -dBoot also runs the audit retention sweep, which is why a deploy is when expired rows are removed. The default is to keep audit rows indefinitely, which is the honest default for an install running its own database: the operator already holds the disk and the data, and silently deleting their history on upgrade would be the surprising choice. Set a finite window per workspace to bound it.
Checking it came up
{
"app": true,
"database": true,
"runner": false,
"embedded": { "toolkits": 0, "of": 31, "tools": 0, "attempted": 0, "lastError": null },
"commit": null,
"checkedAt": "2026-08-27T09:12:44.108Z"
}databaseis a real query, not a ping.runneris whether the sandbox runtime answered. False is expected on a deployment without one.embeddedis how much of the catalogue can be searched by meaning rather than by words.commitis which build is answering, baked in at image build time and null when the application runs from a working tree.
The endpoint has no credential in front of it, so it reports counts and never names, and any string long enough to be a secret is scrubbed out of the last embedding error.
Telemetry
A self-hosted install sends an anonymous usage ping to toolspoke.com/api/telemetry on boot and then once a day. The table below is the complete payload: no names, no emails, no URLs, no tool data, and nothing about what the install is connected to. The sender is scripts/telemetry.mjs in the repository, short enough to read rather than trust.
| Field | What it is |
|---|---|
instanceId | A random UUID minted on first boot and stored in your database. Derived from nothing, and its only job is to let us count one install, still alive, across days. |
version | The app version, so we know which releases are actually running in the wild. |
users | A count of user accounts. A number, nothing about who they are. |
workspaces | A count of workspaces. Same. |
TOOLSPOKE_TELEMETRY=offThe ping can never affect your install: it is fire-and-forget with a five-second deadline, runs beside the server rather than in any request path, and a receiver that is slow, gone, or blocked by your firewall costs you nothing.
Sandboxed execution needs Harborbox
Toolspoke does not ship a sandbox runtime. Two kinds of tool need one: CLI toolkits, which run a pinned binary with arguments an agent supplies, and stdio MCP servers, which are booted as a process and spoken to over stdin and stdout. Both are executed by Harborbox, which is a separate service with its own deployment.
Point HARBORBOX_URL and HARBORBOX_API_KEY at it and those tools work. Leave them unset and the rest of the product is unaffected.
| Tool kind | Without Harborbox | With Harborbox |
|---|---|---|
| REST API toolkits, over HTTPS | Works | Works |
| Streamable HTTP MCP servers | Works | Works |
| CLI toolkits | Unavailable | Works |
| stdio MCP servers | Unavailable | Works |
A deployment without Harborbox is a complete gateway for everything reachable over HTTP, which is most of the catalogue. It is not a partial install or a degraded mode. It simply cannot run the tools that need a machine to run on, and it says so: runner is false at /api/health, and a call that needs the sandbox is refused rather than silently doing nothing.
Hardening a public deployment
- Terminate TLS in a reverse proxy. The container serves plain HTTP on port 3000.
- Do not publish PostgreSQL to a public interface. The self-hosting compose file does not.
- Replace every example value. There are no default credentials in this product, and there should be none in your environment either.
- Review each connector you enable and the scopes it asks for before anyone installs it.
- Back up the database and the two secrets together.
Once it is up, the next step is connecting an agent. Use your own origin in place of toolspoke.com in both address forms on that page.