# 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](/providers) page.
The [backend modules](/operator-policy) 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.
