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 machine, so two reviewers can poke at two different PRs without stepping on each other’s writes. Close the PR and the machine is deleted.

Here GitHub Actions starts the preview. You can also skip GitHub Actions and run it on boxd, which is faster.

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 snapshot of your app: 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:

Save a snapshot, then start a machine from it. Booted in 4ms.

The machine starts 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. An API key from boxd auth keys create acts as you, so the preview is your private machine: with Git connected, the git fetch inside it works against private repos with nothing extra to configure. On a shared machine, git uses the GitHub App instead.

Wire it into CI

Store the key as a repo secret:
Then add one workflow. It brings the preview up on every push to the PR, comments the URL, and removes the machine when the PR closes:
.github/workflows/preview.yml
Any other CI works the same way: run the first script when a PR opens or gets a push, the second when it closes.

Or skip GitHub Actions

The fastest setup runs on boxd itself. An automation receives the pull request event through the boxd GitHub App and runs the same steps, so no runner has to start first and there is no API key to store.

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.
A preview asks for a bot protection check before it wakes, and with the On mode while it runs too. Reviewers pass it in their browser, but headless Playwright runs and other CI jobs may not. Turn it off for previews that automated jobs hit with boxd machine config set myapp-pr-482 bot-protection off.

Preview data without a staging database

A preview needs data, and there are two ways to give it its own copy. Keep the data on the machine. Run a local database on the source machine before you snapshot it. A snapshot and a fork copy the disk, so every preview starts with that data and its writes stay inside that one preview. The next recreate resets it. Branch a hosted database. If your data lives in a hosted Postgres that can branch, such as Neon, create a database branch for each pull request and pass its connection string to the preview machine. The branch is a copy-on-write copy of your data, and you delete it with the machine when the pull request closes.
Opening or pushing to a pull request runs a GitHub Actions workflow. It creates a boxd machine from your snapshot and a Neon branch from your main database, and connects them with DATABASE_URL. The preview URL is posted on the pull request. Closing the pull request deletes the machine and the branch.
Two browser windows with the same notes app. The app on main lists three notes. The preview of pull request 1 lists the same three notes with checkboxes, one of them ticked, and a fourth note written on the preview.

From a test run. Behind: the app on main. In front: the preview of pull request 1 on its own Neon branch. The PR's migration added the checkboxes, and the last note exists only on the preview.

The workflow and scripts below extend the ones above. On open, preview-up.ts creates a Neon branch named myapp-pr-<number> and creates the machine with that branch as its DATABASE_URL. On a push it keeps the branch, so a reviewer’s rows survive the redeploy. On close, preview-down.ts deletes the machine and the branch.You need a Neon project with one database and one role, a Neon API key stored as the NEON_API_KEY secret, and the project ID as the NEON_PROJECT_ID variable. Add both SDKs with your app’s package manager: npm install -D @boxd-sh/sdk @neon/sdk. The example app reads DATABASE_URL from ~/myapp/.env and runs npm run migrate.
A branch is a full copy of its parent, with the same role passwords, and a preview URL is public. If your main branch holds real user data, branch from one with scrubbed data instead: pass its ID as parentId to createAndConnect. Point the snapshot machine itself at a development branch, not production.

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.

Previews sleep when not in use

The script sets a 5-minute auto-suspend and a 30-minute auto-hibernate, so an idle preview goes to standby and then hibernates. The next click on its link wakes it, and only that one.

Ten previews, all hibernated. Opening one wakes only that one.

FAQ

Very little, on two levels. The 5-minute auto-suspend puts an idle preview on standby, where it bills for memory and disk but no vCPU, and clicking the link resumes it in sub-millisecond time. The 30-minute auto-hibernate then moves it to disk, where it bills for disk only, 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 source machine counts toward it too. Hibernated previews count as well.
The preview is created from the latest 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.