hive

hive · docs

A wrangler-shaped CLI for celld — Deno's self-hosted, distributed Durable Objects daemon. Bring your existing Worker; hive tells you if it runs on celld, then deploys and runs it on your own hardware.

Go, stdlib only. Single static binary, zero dependencies, instant cold start.

Quickstart

What you need

Cloudflare is optional: cf login/init/cf tunnel automate R2 + Tunnel + DNS, but any S3 credentials and any reverse proxy (caddy, nginx, your own tunnel) work just as well. On exe.dev, hive exe handles VMs, proxy, and domains instead.

Install

curl -fsSL https://hive.butttons.dev/setup.sh | bash

Installs the latest release binary to ~/.local/bin/hive (checksum-verified). Override the destination with HIVE_INSTALL_DIR.

Or build from source:

CGO_ENABLED=0 go build -o hive .

Cross-compiles to GOOS/GOARCH = linux/amd64, linux/arm64, darwin/arm64 — same targets as celld.

First deploy

hive add myapp            # scaffold app, allocates a free port
cd myapp

# credentials: either export AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY /
# S3_ENDPOINT / AWS_REGION / CELLD_BUCKET yourself, or provision with:
hive init --bucket mybucket --access-key ... --secret-key ... --endpoint ...

hive deploy               # tsc → celld deploy → restart node → health gate
curl http://127.0.0.1:<port>/

There is no watch mode or HMR — the restart is the reload. The loop is edit → hive deploy → curl.

Config model

Two files per app, same directory:

{
  "hive": { "port": 8101, "domain": "app.example.com", "server": "user@box", "dir": "/opt/apps/myapp", "backend": "docker" }
}

port is required. domain, server, dir, backend are optional (local-only apps skip them). When server is set, dir is the absolute path of the app directory on the server; if omitted, the local path is used verbatim, which only works when the box shares the same filesystem layout.

Monorepos

Run hive from a workspace root to operate on the whole fleet. Discovery order:

  1. pnpm-workspace.yml / pnpm-workspace.yaml packages: globs.
  2. Root package.json workspaces field (array or {"packages":["..."]} object).

A directory counts as a hive app only if its package.json contains a "hive" block. Patterns support a single trailing /* (e.g. apps/*).

hive status and hive ui show fleet views from a workspace root and single-app views from an app dir. hive deploy all deploys every app sequentially, continuing past failures. --filter <name> matches the app's package.json name or directory basename and operates on one app even from the root. If no workspace file exists, --packages <glob> enumerates apps manually.

Environment variables

var purpose
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY S3 credentials (fed from the process env or ~/.config/hive/<app>.env)
S3_ENDPOINT / AWS_REGION S3 endpoint / region (auto for R2)
CELLD_BUCKET Fleet bucket; nodes use s3://<bucket>/<app>
CLOUDFLARE_API_TOKEN Cloudflare auth source of truth — wins over the stored OAuth token
HIVE_CF_CLIENT_ID Override the compiled-in OAuth client ID
HIVE_LOGIN_PORT Override the OAuth callback port (tests only)

Commands

Every command takes --json and prints machine-readable output. Agents are the primary user.

command flags what it does
hive add <name> --port, --force Scaffold a new app (wrangler.jsonc + index.ts + tsconfig + package.json), allocate a free port
hive deploy --docker, --local, --no-restart, --filter Typecheck (tsc -b) → celld deploy → restart the node → wait for /.well-known/celld/health (30s gate). Prints the version ID. On failure, prints the node's recent logs. --no-restart uploads only, for externally supervised nodes
hive deploy all --docker, --local, --packages Deploy every workspace app sequentially, continuing past failures
hive up --docker, --local Start the node. Idempotent; config drift → restart
hive down --local Stop the node gracefully (SIGTERM; celld drains in-flight work)
hive status --local, --filter App config + node state; from a workspace root, show a compact fleet table
hive ui — Local dashboard; fleet view from a workspace root, single-app view from an app dir
hive init --bucket, --access-key, --secret-key, --endpoint, --region, --jurisdiction, --force Provision bucket credentials into ~/.config/hive/<app>.env (0600)
hive env --json, --tunnel, --name Print the app's effective env vars, shell-sourcable — hive env --tunnel > .env feeds docker compose or CI secrets
hive bootstrap — Install hive + celld at ~/.local/bin on the app's server (idempotent; re-run to upgrade)
hive cf login --status, --export, --no-browser Cloudflare OAuth consent (PKCE); stores a refreshable token
hive cf tunnel --name, --ssh Create/sync a remotely-managed Cloudflare Tunnel: ingress rules + DNS, prints the box install command
hive exe new <name> — Create an exe.dev VM (idempotent) and wait for its DNS to propagate
hive exe share --private Point the exe.dev HTTPS proxy at the app's port; public by default
hive exe domain — CNAME the app's domain to the VM (DNS-only, via Cloudflare creds) and register it with exe.dev
hive version — Print the hive version. Release binaries are stamped; remote commands warn when the box's hive differs — hive bootstrap syncs it

hive init credential chain

In order — first match wins:

  1. --access-key + --secret-key + --endpoint flags — zero Cloudflare calls, works with any S3 provider.
  2. Reuse from a sibling app: an existing ~/.config/hive/<app>.env with a matching CELLD_BUCKET.
  3. Cloudflare path: bucket create (if missing) + S3 key mint. The mint needs CLOUDFLARE_API_TOKEN with API Tokens Edit; OAuth tokens can't mint keys (Cloudflare's self-managed OAuth catalog has no API Tokens scope).
  4. Otherwise: a deep link to the R2 S3-keys page; paste the pair with --access-key/--secret-key.

hive cf login

Self-managed Cloudflare OAuth (authorization code + PKCE, no client secret). Default public client ID is compiled in; override with HIVE_CF_CLIENT_ID (register your own client for production use). Scopes: argotunnel.write dns.write zone.read workers-r2.write. Tokens live at ~/.config/hive/auth.json (0600) and refresh automatically. --export prints export CLOUDFLARE_API_TOKEN=… instead of saving.

Running nodes

Supervise containers yourself? examples/docker-compose.yml runs a node from the official ghcr.io/denoland/celld image plus a cloudflared sidecar; hive env --tunnel > .env generates its credentials and deploys become hive deploy --no-restart && docker compose restart <app>.

Caution

One node per fleet bucket prefix, ever. Two nodes on the same s3://bucket/<app> prefix produce DurableObjectRoutingError: owner unreachable.

Remote operation & CI

Set "server": "user@box" in the hive block and commands proxy to the hive binary on the box over SSH (ssh box hive <cmd> --local). No RPC protocol; the same backends run there. celld deploy itself is bucket-direct from anywhere — only restart and health checks touch the box. --local forces local operation even when server is set.

When the server's app directory differs from the local one, set "dir": "/absolute/path/on/server" in the hive block. It must be absolute (no ~). On deploy/up, hive creates the directory and syncs package.json + wrangler.jsonc there before running the remote command. down and status also use dir when set.

Box setup: hive bootstrap installs hive + celld at ~/.local/bin on the server (re-run to upgrade). Add docker yourself if wanted. Credentials sync over SSH as ~/.config/hive/<app>.env (0600).

CI: a stock GitHub-hosted runner runs hive deploy; it reaches the box via SSH through the Cloudflare Tunnel (hive cf tunnel --ssh adds the ssh.<domain> ingress rule; sshd bound to loopback, cloudflared as SSH ProxyCommand). Secrets: bucket creds, SSH key, tunnel token.

Going to production

Starting from a plain index.ts + wrangler.jsonc, the whole journey:

hive init                   # one-time: port + bucket credentials
# point the app at a box in package.json:
#   "hive": { "port": 8101, "server": "user@box", "dir": "/opt/apps/myapp", "domain": "app.example.com" }
hive bootstrap              # one-time: hive + celld on the box
hive deploy                 # ship, restart, health-gate — over ssh

Then pick an ingress — all three end at the same 127.0.0.1:<port> listener:

Updating is the same command forever: edit index.ts, hive deploy, done — new version ID, node restart, health gate before it reports ok. CI is a stock GitHub-hosted runner with bucket creds + an SSH key running hive deploy; see Remote operation for the tunnel-secured SSH variant.

exe.dev

hive exe drives the exe.dev control plane over its SSH CLI. exe.dev VMs ship with a public hostname, TLS, and an HTTPS front door, so the Cloudflare Tunnel is unnecessary there — the front door is the ingress. Full flow for a fresh VM:

# hive block: { "port": 8101, "server": "mybot.exe.xyz",
#               "dir": "/home/exedev/apps/mybot", "domain": "mybot.example.com" }
hive exe new mybot        # create the VM, wait for DNS
hive bootstrap            # install hive + celld on it
hive deploy               # ship, restart, health-gate over ssh
hive exe share            # https://mybot.exe.xyz → :8101, public (or --private)
hive exe domain           # mybot.example.com live: DNS-only CNAME + registration

exe domain uses your Cloudflare credentials to create the CNAME and hard-requires proxied: false — exe.dev terminates TLS itself and orange-cloud records break it. Without cf credentials it prints the exact record to create and exits; re-run after creating it. Registration is verified via exe.dev's domain ls and retried, since their resolver lags yours.

The --private share mode keeps the proxy behind exe.dev login — the right default for internal tools.

Security notes