Skip to content
Sandbox SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Boat

Run persistent Ubuntu VMs for long-running agents on Boat. Size presets, durable background processes, and cookie-authenticated previews.

boat() runs each sandbox as a persistent Ubuntu VM on Boat, with Node.js, Git, and Docker installed.

Install

npm install @opencoredev/sandbox-sdk
pnpm add @opencoredev/sandbox-sdk
yarn add @opencoredev/sandbox-sdk
bun add @opencoredev/sandbox-sdk

The adapter calls Boat’s HTTP API itself, so there is nothing else to install. Set BOAT_API_KEY on your server, and keep it out of browser bundles.

Quickstart

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat());
const result = await box.run("uname -a");
console.log(result.stdout);

await using deletes the VM when the block ends.

What it supports

Feature Support Notes
Commands and streaming Yes Live output
Files Yes mode is applied before the bytes land
Background processes Yes
Preview URLs Yes Send the returned Cookie header
Pause and resume No
Snapshots No
Machine size Partial Presets only
Network policy No Only "all"
Hard lifetime (timeoutMs) No Set the ttlSeconds auto-stop instead
Idle timeout (idleTimeoutMs) No
Keys Best-effort Stored in the VM’s display name
Terminal No

The capability matrix compares these with every other provider.

Run commands

Await box.run for the result, or iterate it to stream output as it arrives.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat());
const result = await box.run("ls -la", { cwd: "/workspace", timeoutMs: 30_000 });
console.log(result.exitCode, result.stdout);

for await (const chunk of box.run("for i in 1 2 3; do echo $i; sleep 1; done")) {
  process.stdout.write(chunk.text);
}

A timeout or abort stops the command’s process tree.

Files

Relative paths resolve against /workspace.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat());
await box.files.write("notes/todo.txt", "ship the docs\n");
console.log(await box.files.read("notes/todo.txt"));
console.log(await box.files.list("notes"));

await box.files.write(".env", "API_TOKEN=...\n", { mode: 0o600 });

With mode, the file gets its permissions before any contents are written, so the contents are never readable by other users. Writes run as the sandbox user, so root-owned paths such as /etc fail. Write those with box.run("sudo ...").

Background processes and previews

No ports to declare. The preview needs the Cookie header in preview.headers on every request. Treat it as a secret.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat());
await box.start("npx serve -l 3000", { name: "web", ready: { port: 3000 } });
const preview = await box.preview(3000, { waitMs: 10_000 });
const response = await fetch(preview.url, { headers: preview.headers });
console.log(response.status);

Pause, resume, and snapshots

This adapter has no pause, resume, or snapshots, and the methods don’t exist on its sandbox type. Boat’s own stop and snapshot endpoints are reachable through box.native. Code that accepts any provider can check first:

import { createSandbox, hasCapability } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat());
if (hasCapability(box, "pause")) await box.pause();

Size, network, and lifetime

Preset Boat type CPUs Memory Disk
small small 2 4096 MiB 12 GB
medium default 4 8192 MiB 50 GB
large large 8 16384 MiB 125 GB
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat(), { size: "large" });

Exact sizes, network other than "all", idleTimeoutMs, and timeoutMs fail with unsupported.

By default Boat stops a VM an hour after it starts. Set ttlSeconds to change that, or null to turn auto-stop off. The timer restarts while Boat’s own agent works in the VM, so it caps cost but is not a hard lifetime. A stopped VM has no SDK resume, so destroy it.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(
  boat({ ttlSeconds: 15 * 60, snapshots: false, monitoring: false }),
);

Configuration

Option Type Default Env var
apiKey string BOAT_API_KEY
baseUrl string https://boat.dev/api/v1 BOAT_API_BASE
machine "small" | "default" | "large" | "xlarge" "default"
ttlSeconds number | null 3600
snapshots boolean true
monitoring boolean Account setting
org string Your active wallet
requestTimeoutMs number 90_000

A per-create size wins over machine. xlarge (16 CPUs, 32 GB) needs a $100+ plan. ttlSeconds takes 1 to 2,592,000 (30 days). snapshots: false means Boat never captures the disk, and a stop erases it. monitoring: false means Boat keeps no command output, process list, or metrics; live output still streams. org bills the VM to an organization, by id or name. apiUrl and type are deprecated aliases.

Call the API directly

box.native calls any Boat endpoint with the adapter’s base URL, key, retries, and error mapping. Paths are relative to https://boat.dev/api/v1, and {sandboxId} fills in from the sandbox. Boat has one plane, "api". See the native API reference for every option.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat());
await box.run("echo report > /tmp/report.txt");

const history = await box.native.request<{ commands: { command: string; status: string }[] }>(
  "GET",
  "/sandboxes/{sandboxId}/command-history",
  { query: { limit: 5 } },
);
console.log(history.commands.map((entry) => entry.status));

const file = await box.native.fetch("GET", "/sandboxes/{sandboxId}/artifacts", {
  query: { path: "/tmp/report.txt" },
});
console.log(await file.text());

request returns the JSON body and fetch the raw Response. A non-2xx status rejects with SandboxError.

For account calls with no sandbox, use the provider’s api:

import { boat } from "@opencoredev/sandbox-sdk/boat";

const limits = await boat().api.request("GET", "/limits");
console.log(limits);

To use the official SDK instead, install @boatdev/sdk and pass its import to sdk. On a sandbox you get a BoatApi client set up with the same base URL and key, plus the sandbox’s metadata. On provider.api you get the client alone.

import { createSandbox } from "@opencoredev/sandbox-sdk";
import { boat } from "@opencoredev/sandbox-sdk/boat";

await using box = await createSandbox(boat());
const { client, sandbox } = await box.native.sdk(() => import("@boatdev/sdk"));
console.log(sandbox.state);
const { snapshots } = await client.listSandboxSnapshots({ sandboxId: box.id });
console.log(snapshots.length);

In earlier betas, box.native was { sandbox, client }. That pair now comes from box.native.sdk(() => import("@boatdev/sdk")).

Limitations

  • Keys are best-effort, and the encoded key and labels must fit a 120-character name.
  • No custom sizes, network policies, idle timers, hard lifetimes, pause, snapshots, or terminals.
  • File writes run as the sandbox user, so root-owned paths fail.
  • A native stop ends processes, and /workspace and /tmp records may not survive it.

How it works

  • Sandboxes are created with noEnv: true, so they get none of your account’s saved secrets. env and secrets still apply.
  • Boat has no labels, so the key and labels are compressed into the display name. Renaming the VM in Boat hides it from the SDK.
  • Create retries up to three times with the same idempotency key. Commands are never replayed. Reads, file writes, and deletes retry on 429, 502, 503, 504, and dropped connections.
  • Command output streams from Boat’s NDJSON attach endpoint. Boat ends each connection after 10 minutes; the adapter reopens it from the last byte offsets, so no output is lost or repeated. A dropped connection or 60 seconds of silence is reopened the same way for up to 60 seconds.
  • stdin and file contents never travel in command text, which Boat keeps in its history. Boat’s file API takes at most 5 MiB per write and only paths under /home/user or /tmp. A write of up to 4 MiB under those paths, without mode, goes straight to it. Anything else, and all stdin, is uploaded in 4 MiB parts to a new 0700 directory under /tmp. A guest command then copies the file into place, or the shell opens the stdin file and deletes it before your command starts.
  • Reads above 1 MiB use a signed download URL; smaller reads use guest commands.
  • destroy() deletes the VM and its snapshots. Boat purges the data asynchronously.

Next steps

Was this page helpful?