About hh.pe

hh.pe (Hot Handoff Platform Engine, sandboxo in the code) is a self-hostable-to-be Deno Deploy ripoff. It speaks enough of Deno Deploy's APIs that the unmodified deno deploy CLI, jsr:@deno/sandbox and npm:e2b work against it, and it runs everything on one host you control: every build, every app instance and every interactive sandbox gets its own gVisor sandbox.

The other sections are Deno's own Deno Deploy and Deno Sandbox documentation (MIT, from denoland/docs). Read them with this page in mind: where they say console.deno.com, think of the hh.pe web UI, and anything listed under [what-is-missing] is not there.

How it works

  1. Upload: deno deploy sends a manifest of file hashes and then uploads only the files the server does not have yet.

  2. Build: install and build commands run in a pre-warmed build sandbox, with network for jsr and npm, a timeout and optional cgroup memory and CPU limits. The result is a self-contained directory, packed into a read-only EROFS image, so the app starts with --cached-only.

  3. Promote: a production revision becomes the app's current revision once its build succeeds.

  4. Serve: the first request takes a pre-warmed runtime sandbox, mounts the build image into it and proxies to it. Warm means V8 has already booted, using Deno's prewarm control socket (the mechanism Deno Deploy itself uses), so a cold start takes hundreds of milliseconds.

  5. Idle: an instance is stopped after zoned's idle_ms without requests, and the pool refills in the background.

Apps are served on <app>.<domain>, previews of a single revision on <app>--<rev>.<domain>. Records live in Redis and blobs in S3, see State directory. The control plane (sandboxod), the sandboxes (zoned, one per availability zone) and the routers that take public traffic over HTTPS (obice, envoy driven by its own xDS server) are separate programs that can run on separate hosts; zones only dial out, so they can sit behind NAT, see Zones.

What works

  • deno deploy with DENO_DEPLOY_ENDPOINT: deploys (production and preview), create, apps, deployments, env, logs, login. The deno.json deploy section (install, build, predeploy, runtime, buildTimeout) overrides the app config per revision.

  • Quick deploy links compatible with Deno Deploy's /new?clone=..., for public https:// git repositories. Empty settings are detected from the repository.

  • Sandboxes through jsr:@deno/sandbox, deno deploy sandbox and npm:e2b, all on one core: a sandbox created through one API can be used through the other. Volumes, timeouts, exposeHttp and an SSH gateway for exposeSsh.

  • Logs and OpenTelemetry: app, build and sandbox logs are stored in Loki; apps export traces and metrics the way they do on Deno Deploy, without endpoints or credentials in the app; see Observability.

  • Web UI: apps, revisions with promote and redeploy, live build and app logs, config and env editors, sandboxes with a web terminal, and these docs at /docs.

  • Cron: Deno.cron() definitions are discovered when a revision is built and run on the production revision, with retries, execution history and traces. Differences: no branch timelines, names are capped at 64 characters by Deno, and the app must call Deno.serve.

What is missing

  • More than one control plane host, latency-aware routing (a router sends an app's requests to its zones by replica count, not by distance), authentication on tunnels and route fetches beyond a private network, moving volumes between zones.

  • Custom domains: routers serve <domain> and *.<domain> with one certificate.

  • WebSockets and other HTTP upgrades through the app router.

  • KV, databases, queues, cloud connections, OIDC, deno run --tunnel.

  • GitHub integration beyond public clones: no GitHub app, no builds on push.

  • Env contexts beyond Local: production and preview always share variables; secrets are write-only but stored unencrypted on the host.

  • Framework presets other than Next.js: give install, build and runtime settings explicitly.

  • Sandboxes: booting from a snapshot, bootable volumes, VS Code, spawnDeno, fetch, deploying from a sandbox.

  • Volume capacity is recorded but not enforced.

Running

nix develop
pnpm install
deno task ui:build
deno task docs:build
process-compose up
SANDBOXO_TOKEN=secret deno task dev

dev.ts runs sandboxod and two zones in one process, with the config the dev shell generates (SANDBOXO_DEV_CONFIG); sandboxod/main.ts and zoned/main.ts run them separately, each with its own config file, see Configuration. The web UI, the API and the Deno Deploy endpoint listen on http://127.0.0.1:8001, apps on https://<app>.sandboxo.localhost:8443 (an app named root on https://sandboxo.localhost:8443 itself). Redis, Garage, the telemetry backends and the router come from process-compose up. The router's certificate is signed by a development CA (packages.dev-tls): deno in the dev shell trusts it through DENO_CERT, curl needs --cacert $SANDBOXO_DEV_TLS/ca.pem.

export DENO_DEPLOY_ENDPOINT=http://127.0.0.1:8001 DENO_DEPLOY_TOKEN=secret
deno deploy create --org local --app hello --source local \
  --runtime-mode dynamic --entrypoint main.ts --region us
deno deploy --org local --app hello --prod
curl --cacert $SANDBOXO_DEV_TLS/ca.pem https://hello.sandboxo.localhost:8443/

Deno sandboxes additionally need DENO_SANDBOX_ENDPOINT=http://127.0.0.1:8001 and DENO_DEPLOY_ORG=local. For e2b, set E2B_API_URL=http://127.0.0.1:8001, E2B_SANDBOX_URL=https://sandboxo.localhost:8443 and E2B_API_KEY=secret.

Caveats

  • The deno deploy CLI keeps one keychain token for all endpoints, so deno deploy login against hh.pe replaces your real Deno Deploy token. Prefer DENO_DEPLOY_TOKEN, which is never stored.

  • The deno deploy CLI evaluates server responses as code: only point it at an hh.pe you trust.