Skip to main content
Every pull request gets its own machine running the branch’s exact code, live at a predictable *.boxd.sh URL that gets posted on the PR. Reviewers click it and land on a full Linux VM, so two reviewers can poke at two different PRs without stepping on each other’s writes. Close the PR and the machine is destroyed. The whole thing is two small SDK scripts on top of one primitive: creating a machine from a snapshot.

How it works

You maintain a golden image: a snapshot of a machine with your app installed and running, re-saved on every push to main so it always reflects your latest code. That guide is the prerequisite for this page. On every PR, a script creates a machine from the latest snapshot, named for the PR:
The machine wakes with the app already serving main’s code, since a snapshot captures memory and disk together. The script then checks out the PR branch inside it, rebuilds what the diff needs, and the preview is live at https://myapp-pr-482.boxd.sh. On new pushes the machine is recreated from the latest snapshot, and because the name is reused, the URL stays stable across the PR’s whole life. On close, one command removes it.

The scripts

Two small scripts cover the full lifecycle. The recommended way to run them is through the TypeScript or Python SDK, which reads a BOXD_API_KEY from the environment and handles authentication for you. Bring a preview up, or refresh it after a push:
preview-up.ts
Tear it down when the PR closes:
preview-down.ts
Swap the rebuild command (npm ci && sudo systemctl restart myapp) for whatever your stack needs. With the GitHub integration connected, the git fetch inside the preview machine works against private repos with nothing extra to configure.

Wire it into CI

You can wire this into GitHub Actions (or any CI) with two small jobs: run the first script on pull_request opened, synchronize, and reopened, and the teardown on closed, passing the PR number and branch as arguments. Store the key as a repo secret first:
Post the printed URL back on the PR with gh pr comment, and scope concurrency per PR number so rapid pushes don’t race each other. You can of course also run the boxd part on a boxd VM, for example on a self-hosted runner or a small webhook listener living there. Inside a machine, new Boxd() authenticates automatically, so the key disappears entirely and the scripts run unchanged.

Patterns

A stable URL per PR

The machine is named for the PR number, so each PR keeps https://myapp-pr-<n>.boxd.sh across every push. That makes it a steady target for screenshot diffs and Playwright runs, and reviewers keep refreshing the same link.

Preview data without a staging database

Run a local database on the golden machine before you snapshot it. Every preview then inherits a copy of that data, and writes stay inside that one preview. Reset comes free with the next recreate.

More than a URL

Each preview is a full machine you own. ssh myapp-pr-482.boxd gets you a shell in the exact environment the reviewer is looking at, and boxd machine exec lets other jobs run tests against it.

FAQ

Very little, on two levels. The 5-minute auto-suspend freezes an idle preview in RAM at near-zero cost, and clicking the link resumes it in sub-millisecond time. The 30-minute auto-hibernate then parks it on disk, where it costs effectively nothing beyond storage, and the next click wakes it in about 85ms. A PR sitting open over the weekend costs effectively nothing.
You get 5 machines, one per PR. The default cap is 50 machines per organization, extendable on request, and the golden machine counts toward it too.
The preview is created from the latest golden snapshot, which your refresh workflow re-saves on every push to main. The PR branch is then checked out on top, so the diff against main is exactly what the rebuild has to cover.
Recreating from the snapshot gives every push a clean, known starting state for the price of one boot, and the reused name keeps the URL stable. If your rebuild is expensive and pushes are frequent, reuse the machine and run the checkout step alone.