Skip to content

Self-Hosting

The open-source backend is the remote control plane. The CLI always talks to a backend; it does not provision cloud machines locally. The same backend that powers the hosted app.boxhaven.dev self-hosts with your own provider credentials and no built-in limits.

The browser app is built with TanStack Router and TanStack Query. It is the console/auth surface: login, signup, CLI device approval, invitations, and authenticated box/team/image views. The API serves the built console from dist-app, and teams can host the static docs/ build alongside it.

In production the intended split is boxhaven.dev for the paid-service website, docs.boxhaven.dev for documentation, app.boxhaven.dev for the console/auth app, and api.boxhaven.dev for the API. The API also serves the built console app from dist-app for simple self-hosted deployments.

The open-source console shows a compact update banner when the backend reports a newer public BoxHaven GitHub release. The backend caches successful checks for 24 hours, retries failed checks after an hour, and returns no update when offline, so release discovery never prevents the console from loading. Banner links open the exact GitHub release in a new tab.

Run Locally

bash
cd backend
npm ci
BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
DIGITALOCEAN_ACCESS_TOKEN=dop_v1_example \
RESEND_API_KEY=re_replace_with_a_key \
npm run dev

Run the web app during development from a second shell:

bash
npm run dev:app

By default the server listens on 127.0.0.1:8787. Better Auth data, machines, images, and the lifecycle outbox share one SQLite database at ~/.local/state/boxhaven/boxhaven.sqlite.

Then point the CLI at it:

bash
bh login --backend-url http://127.0.0.1:8787
bh create work

Run With Docker Compose

From the repository root:

bash
export BETTER_AUTH_SECRET="$(openssl rand -hex 32)"
export DIGITALOCEAN_ACCESS_TOKEN=dop_v1_example
export RESEND_API_KEY=re_replace_with_a_key
export BOXHAVEN_VERSION="$(git describe --tags --match 'v[0-9]*' --always)"
docker compose -f docker-compose.backend.yml up --build

The Compose service publishes 127.0.0.1:8787 by default and persists backend state in the boxhaven-backend-data Docker volume. Override the host bind with BOXHAVEN_BACKEND_PORT, for example BOXHAVEN_BACKEND_PORT=127.0.0.1:8877. When the public URL changes, also set BETTER_AUTH_URL, BOXHAVEN_APP_URL, and BOXHAVEN_API_URL so browser login links point at the reachable host. Set BOXHAVEN_DOCS_URL before docker compose ... up --build if the console footer should link to an internal docs site instead of https://docs.boxhaven.dev.

A dummy DIGITALOCEAN_ACCESS_TOKEN is enough for build, startup, health, and read-only API checks. Creating boxes from this stack still requires a real DigitalOcean token and a CLI login token for the local backend.

Build A Distribution

The backend is also a local npm package named @boxhaven/backend. An internal or hosted distribution can depend on it and call startBackendFromEnv({ modules, appDir }) from its own entrypoint. Compile-time modules can add sequential migrations, private tables, authenticated Fastify routes, and one in-process commercial policy. Their migrations share the boxhaven_migrations ledger and SQLite database with core, while a separately built appDir replaces the open-source browser UI for that distribution.

The standard entrypoint loads no modules. There is no runtime switch that can enable private functionality in a self-hosted build, and private modules do not need to duplicate the core API, auth, provider, SSH, or CLI implementations.

Environment Variables

  • BETTER_AUTH_SECRET: required signing secret for Better Auth sessions.
  • BETTER_AUTH_URL: public auth base URL, default http://<listen>/v1/auth.
  • BETTER_AUTH_TRUSTED_ORIGINS: comma-separated trusted browser origins.
  • RESEND_API_KEY: required Resend API key. Password signups must follow an emailed verification link before signing in.
  • BOXHAVEN_EMAIL_FROM: transactional From address, default BoxHaven <noreply@boxhaven.dev>.
  • BOXHAVEN_EMAIL_VERIFICATION_EXPIRES_SECONDS: positive verification-link lifetime, default 3600.
  • BOXHAVEN_APP_URL: public console/auth app URL, default derived from BETTER_AUTH_URL in direct runs and http://127.0.0.1:8787 in Compose.
  • BOXHAVEN_API_URL: public API URL, default derived from BETTER_AUTH_URL in direct runs and http://127.0.0.1:8787 in Compose.
  • BOXHAVEN_VERSION: current backend version used by the public /v1/version release-status endpoint. Set it from CLI release tags using git describe --tags --match 'v[0-9]*' --always; this excludes separately versioned skills. The production deploy script derives it automatically and verifies the running backend received it.
  • BOXHAVEN_DOCS_URL: public documentation URL used by console footer links in Docker builds. Set this when self-hosting internal docs; otherwise the app links to https://docs.boxhaven.dev.
  • BOXHAVEN_BACKEND_CORS_ORIGINS: comma-separated browser origins allowed to call the API.
  • BOXHAVEN_PREVIEW_BASE_DOMAIN: optional base domain for generated machine preview hosts, such as at.boxhaven.dev.
  • BOXHAVEN_PREVIEW_TARGET_PORT: machine port that preview hosts proxy to, default 80.
  • BOXHAVEN_DATABASE_PATH: shared SQLite database path, default ~/.local/state/boxhaven/boxhaven.sqlite.
  • BOXHAVEN_BACKEND_LISTEN: listen address, default 127.0.0.1:8787.
  • BOXHAVEN_SSH_CA_KEY: backend SSH user CA private key path, default beside BOXHAVEN_DATABASE_PATH.
  • BOXHAVEN_ADMIN_EMAILS: comma-separated emails granted admin access to the image-management endpoints.
  • BOXHAVEN_MAX_TEAMS_PER_USER: optional positive cap on teams a user owns. Concurrent creates reserve capacity and invitations to someone else's team do not consume an ownership slot.
  • BOXHAVEN_MAX_MACHINES_PER_USER: optional positive per-user cap on existing and provisioning boxes across all of their teams. When the cap is reached, POST /v1/machines returns 403 with { "id": "limit_reached" }. Capacity is released after a definitive provider failure or successful destroy. Hosted distributions set this; self-hosted deployments may leave it unset.
  • BOXHAVEN_COMMERCIAL_POLICY_RETRY_MS: failed policy delivery or policy-requested machine cleanup retry delay, default 30000.
  • BOXHAVEN_COMMERCIAL_POLICY_RECONCILE_INTERVAL_MS: complete active-machine reconciliation and lifecycle-policy evaluation interval, default 300000.
  • BOXHAVEN_BACKEND_PROVIDER: default provider for creates that do not request one explicitly. When unset, the first configured provider is the default (DigitalOcean when both are configured).

Provider credentials and image variables (DIGITALOCEAN_*, HCLOUD_TOKEN, HETZNER_*, BOXHAVEN_REMOTE_IMAGE*) are documented on the Cloud Providers page. The backend modules page documents the build-time extension boundary for distributions that add private models, routes, or policy.

Production DigitalOcean Deployment

The repository includes a production bundle in deploy/digitalocean/ for a self-hosted installation:

  • app.boxhaven.dev for the browser console/auth app
  • api.boxhaven.dev for API and Better Auth routes
  • docs.boxhaven.dev for the static documentation site
  • *.at.boxhaven.dev for generated machine preview URLs
  • Caddy-managed TLS in front of the backend container
  • a Caddy file-server mount for the built docs/.vitepress/dist artifact
  • host-mounted backend and Caddy data under /opt/boxhaven/data
  • a systemd timer that writes daily archives to /opt/boxhaven/backups

Provision

Create an Ubuntu 24.04 Droplet with deploy/digitalocean/cloud-init.yml. Enable DigitalOcean Droplet backups for machine-level restore coverage.

Required DNS records:

text
app.boxhaven.dev.  A  <droplet-ip>
api.boxhaven.dev.  A  <droplet-ip>
docs.boxhaven.dev. CNAME app.boxhaven.dev.
*.at.boxhaven.dev.  A  <droplet-ip>

Configure

Copy env.production.example to .env.production on the server and fill in the secret values:

bash
cp deploy/digitalocean/env.production.example deploy/digitalocean/.env.production

BETTER_AUTH_SECRET must be a long random value and RESEND_API_KEY is required for password-account verification in every self-hosted deployment. Configure a Resend-verified BOXHAVEN_EMAIL_FROM before startup. The backend also needs DIGITALOCEAN_ACCESS_TOKEN so it can create remote VMs for users. The backend SSH user CA is stored at /opt/boxhaven/data/backend/ssh_ca_ed25519 and is included in the backend data backups. Set BOXHAVEN_DOCS_HOST to the documentation hostname, BOXHAVEN_DOCS_URL to the public URL the console should link to, and BOXHAVEN_PREVIEW_BASE_DOMAIN to the wildcard domain above.

Deploy

Deploy the public self-hosted stack from the repository root:

bash
npm run deploy:app

By default the command SSHes to root@app.boxhaven.dev, fast-forwards /opt/boxhaven/app on master, builds the docs site, runs the Compose deploy on the Droplet, and checks the public app, API, and docs health endpoints. It forwards your SSH agent so the Droplet can fetch the private GitHub repo without storing a GitHub token. Override the SSH target with BOXHAVEN_DEPLOY_TARGET=root@<control-plane-ip> or -- --target user@host for self-hosted installs. On the Droplet itself, use npm run deploy:production:local.

BoxHaven operators deploy app.boxhaven.dev from the sibling private boxhaven-hosted repository using npm run deploy:production, followed by npm run deploy:production:verify. The combined deployment includes billing, account limits, and the hosted console. Verify account routes and an authenticated usage request too: public health endpoints do not establish that the hosted module is running.

Distributions can add a build-time module or replace deployment wiring with BOXHAVEN_PRODUCTION_COMPOSE_OVERLAY_FILE and an optional BOXHAVEN_PRODUCTION_COMPOSE_OVERLAY_ENV_FILE. The corresponding flags are --compose-overlay and --compose-overlay-env-file. When an existing backend was deployed with an overlay, deployment and verification require an overlay again. Use the distribution's deployment command to supply its full configuration.

Health Checks And Backups

bash
npm run deploy:production:verify
sudo systemctl status boxhaven-backend-backup.timer --no-pager

Backups are installed through deploy/digitalocean/install-backups.sh and write archives under /opt/boxhaven/backups. The backend data backup uses SQLite's online backup command and requires boxhaven.sqlite and the SSH CA keypair. It validates the copied database before atomically publishing the archive and includes Caddy data when present. The golden remote image includes the sqlite3 CLI so the checked-in backup integrity test can run from a box.

Golden Image Rotation

Remote runtime dependencies belong in the golden VM image. After changing cmd/bh/assets/remote-vm-install.sh, build and activate a new snapshot from a clean committed checkout or pushed ref:

bash
deploy/digitalocean/build-remote-image.sh \
  --env-file deploy/digitalocean/.env.production \
  --set-active

The installer pins Codex CLI 0.153.3 for gpt-6-astra support. After rotating the snapshot, create new boxes to use the updated CLI; existing boxes keep their installed version.

Or use the checked-in npm entrypoint:

bash
npm run deploy:runtime

Each build creates a fresh snapshot and retains existing snapshots. Names are also chosen by teams, so a boxhaven-remote-* prefix does not identify an image as safe to delete. After a successful production smoke, review old snapshots and delete only specific IDs you have confirmed are no longer needed, retaining a known-good golden image for rollback. Retained snapshots incur storage charges.

The runtime deploy creates and snapshots a temporary DigitalOcean builder Droplet, updates BOXHAVEN_REMOTE_IMAGE, then restarts and verifies the backend so new boxes use the image. When an active BOXHAVEN_REMOTE_IMAGE exists, the builder starts from that snapshot by default instead of reinstalling the full OS/toolchain from Ubuntu. Use npm run deploy:runtime -- --full-base-image only for base OS or runtime dependency rebuilds.

npm run deploy:runtime writes BOXHAVEN_REMOTE_IMAGE to the env file as the env-configured default. Team images do not override that default globally; they are selected per box with bh create --image <name-or-id> or the console create form.

Keep the previous snapshot id until the remote lifecycle smoke passes.

Remote Lifecycle Smoke

Run the reusable remote lifecycle smoke against the backend after remote VM, SSH, sync, snapshot, preview, or agent changes:

bash
BOXHAVEN_TOKEN=... \
GH_TOKEN=... \
BOXHAVEN_SMOKE_GIT_REMOTE=https://github.com/<org>/<smoke-repo>.git \
make smoke-remote

The default smoke is intentionally fast: it creates one box from the active snapshot, syncs a temporary Git project, verifies runtime tools, fetches the preview URL, optionally pushes and deletes a temporary GitHub smoke branch, and destroys the box unless BOXHAVEN_SMOKE_KEEP=1 is set.

Useful options:

bash
BOXHAVEN_SMOKE_BACKEND_URL=https://api.boxhaven.dev
BOXHAVEN_SMOKE_TIER=small
BOXHAVEN_SMOKE_PREFIX=my-smoke
BOXHAVEN_SMOKE_KEEP=1
BOXHAVEN_SMOKE_REQUIRE_PREVIEW=0

Use make smoke-remote-full with BOXHAVEN_SMOKE_RESTART_BACKEND_CMD when the agent reconnect path needs coverage. Use make smoke-remote-two-box only for concurrency, provider import, or multiple-machine behavior.

Web Preview

Each hosted box receives a public preview URL when the backend is configured with a preview base domain. The backend warms the preview URL during machine create so Caddy has already completed on-demand certificate issuance before the URL is shown. Public HTTPS and WebSocket traffic terminate at the BoxHaven control plane (Caddy terminates HTTPS on the control-plane Droplet), then the backend proxies plain HTTP/WebSocket traffic to the machine's BOXHAVEN_PREVIEW_TARGET_PORT, default 80. Remote apps do not need to manage public TLS for the preview URL.

Inside the box, commands receive:

  • BOXHAVEN_PREVIEW_URL: the browser URL to share.
  • BOXHAVEN_PREVIEW_HOSTNAME: the public hostname.
  • BOXHAVEN_PREVIEW_TARGET_PORT / BOXHAVEN_WEB_PORT: the machine port to serve, normally 80.
  • BOXHAVEN_WEB_BIND: the bind address to use, normally 0.0.0.0.
  • /run/boxhaven/context.json: structured runtime context with the same preview details under .preview.

Apps should bind HTTP to 0.0.0.0:$BOXHAVEN_WEB_PORT or run a reverse proxy on that port to the app's internal dev-server port. Framework dev-server WebSockets, including Vite HMR, use the same preview URL. The default boxhaven user has sudo access if binding to port 80 is required.

Hosted Versus Self-Hosted

app.boxhaven.dev is the hosted control plane run by the BoxHaven operators. Hosted boxes are provisioned from the operators' cloud provider accounts, with per-user team and active-box capacity policies enforced by the control plane. The same open-source backend self-hosts with your own provider credentials and no built-in limits.

BoxHaven documentation. Source and AGPL-3.0 license text are on GitHub.