opencloudportDocs

Sandboxes

Start Linux machines, write files, run commands and servers, and preview them.

The app needs the Sandboxes service first (POST /apps/{id}/services with type: "sandbox"). See Sandboxes for sizes and how sleeping works.

The sandbox object

{
  "id": "sbxab12cd34ef56gh78ij",
  "object": "sandbox",
  "app_id": "app_…",
  "size": "standard-1",
  "status": "running",
  "idle_timeout_seconds": 900,
  "internet": true,
  "end_user": null,
  "label": "Preview build",
  "image": "default",
  "keep_files": true,
  "keep_dependencies": true,
  "saved_files": { "saved_at": "…", "size_bytes": 48211 },
  "saved_files_note": null,
  "setup_command": "pnpm install",
  "setup_runs": "once",
  "setup": { "ran_at": "…", "exit_code": 0, "output": "Done in 9.4s\n", "note": null },
  "public_preview": false,
  "preview_inject": null,
  "preview_auth": "cookie",
  "preview_embedders": [],
  "preview_url": "https://sbxab12cd34ef56gh78ij-{port}.<preview domain>/?ocp_preview=…",
  "created_at": "…",
  "deleted_at": null
}

status is running, asleep, stopped or deleted. Replace {port} in preview_url with the port your server listens on.

  • Private previews. Unless public_preview is true, preview_url carries a signed link that works for an hour. Opening it lets that browser in for the hour; anyone without it gets "This preview is private". Fetch the sandbox again for a fresh link.
  • Files are kept. With keep_files on (the default), everything in /workspace is saved when the sandbox stops or falls asleep, and is back when it starts again, before anything runs. Installed packages (node_modules and package caches) are kept too, so there's no reinstall; set keep_dependencies: false to save less and install again instead. Up to 2 GB is kept; saved_files_note says when a save was skipped and why. saved_files says when they were last saved. (Sandboxes made before 22 October 2026 left packages out.)
  • Setup command. setup_command runs in /workspace when it starts, before anything else: pnpm install, or tools it needs. With setup_runs: "once" (the default) it runs again only if the command changes or its saved files are gone, since what it wrote into /workspace comes back with them. Use "every_start" for things installed outside /workspace, like system packages (apt-get install -y ffmpeg), which aren't saved. The first command after a start waits for it (up to 10 minutes); setup says how it last went.

Start one

POST /api/v1/apps/{id}/sandboxes

Prop

Type

Returns 201. Needs a positive balance. Up to 50 per app and 200 per workspace.

EndpointWhat it does
GET /api/v1/apps/{id}/sandboxesThe app's sandboxes.
GET /api/v1/apps/{id}/sandboxes/{sandboxId}One sandbox.
PATCH /api/v1/apps/{id}/sandboxes/{sandboxId}Change setup_command and setup_runs (from its next start), and public_preview, preview_inject, preview_auth and preview_embedders (at once, no restart).
GET /api/v1/apps/{id}/sandboxes/{sandboxId}/preview-link?port=5173A fresh signed link to a preview (an hour), with its token and expires_at.
POST /api/v1/apps/{id}/sandboxes/{sandboxId}/stopStop it and stop paying. Its files are saved first if it keeps them.
POST /api/v1/apps/{id}/sandboxes/{sandboxId}/startStart a stopped or asleep one again: its saved files put back (or an empty workspace if it doesn't keep them), same size and variables. Needs a positive balance. A running one is left as it is. (Running a command also starts it.)
DELETE /api/v1/apps/{id}/sandboxes/{sandboxId}Delete it.

Run a command

POST /api/v1/apps/{id}/sandboxes/{sandboxId}/exec runs a command and waits for it.

Prop

Type

{ "data": { "exit_code": 0, "stdout": "…", "stderr": "", "truncated": false } }

A stopped or sleeping sandbox starts again first (with its saved files, if it keeps them).

Output as it happens

With "stream": true the answer is text/event-stream: an event for each piece of output, then one for how it ended.

event: stdout
data: {"type":"stdout","data":"added 214 packages\n"}

event: exit
data: {"type":"exit","exitCode":0}

stderr events look the same, and error ({ "type": "error", "message" }) means the sandbox stopped answering. With the SDK: for await (const chunk of box.execStream('pnpm build')) { … }. MCP tools can't stream: for long jobs an agent starts a process and checks on it.

Files

EndpointWhat it does
PUT /api/v1/apps/{id}/sandboxes/{sandboxId}/filesWrite files. Body: { "files": [{ "path", "content", "encoding"? }] }, up to 500 files and 20 MB. encoding is utf8 (default) or base64. Paths are relative to /workspace.
GET …/files?path=src/index.tsA file's raw contents (not JSON).
GET …/files?path=src&list=1What's in a folder: name, type (file, directory, other) and size.
DELETE …/files?path=distDelete a file or folder.

Between a sandbox and a bucket

Files move straight between the sandbox and one of the app's buckets (bucket is the storage service's svc_… id), binary files included, without passing through you. At most 1,000 files and 50 MB per call.

EndpointWhat it does
POST …/copy-from-bucket{ "bucket", "prefix"?, "path"? }: copy every file under prefix into the sandbox at path (default /workspace), keeping its path below the prefix. Returns { copied, size_bytes, path }.
POST …/save-to-bucket{ "path", "bucket", "prefix"? }: save a file or folder of the sandbox under prefix, keeping paths below the folder, replacing files with the same keys. Returns { saved, size_bytes, prefix }.

Deploy what a sandbox built

POST /api/v1/apps/{id}/sandboxes/{sandboxId}/deploy makes a sandbox's output live: it's packed in the sandbox and deployed like an upload, so nothing is read back through you and binary files are fine. Answers 201 with the deployment once it's live; its source says which sandbox and folders it came from.

Prop

Type

For a TanStack Start app (or anything built with @cloudflare/vite-plugin), build it and give just { "wrangler_config": "dist/server/wrangler.json" }: the server code, its entry, the client files in dist/client and nodejs_compat all come from that file. With a code folder that holds a wrangler.json, it's found by itself; with nothing at all, .wrangler/deploy/config.json (which the plugin writes) points to it. Otherwise give assets_folder, code_folder with main, or both; anything you give wins over the config. If the code fails to start, the deploy is refused with the error and the app keeps its previous code. It uses no time beyond the sandbox's own.

Previews inside your own pages

For platforms that show a sandbox's preview inside their own app, in a frame.

  • Who may frame it. Set preview_embedders to your app's origins, like ["https://app.example.com"]. Previews then send Content-Security-Policy: frame-ancestors 'self' https://app.example.com, so other sites can't show them.

  • Access in a frame. In the default cookie mode, opening the signed link leaves a cookie made for frames: SameSite=None; Secure; Partitioned (each site that frames it gets its own). Where a browser blocks even that, use preview_auth: "url". The token then stays in the preview's address, and a small service worker adds it to the page's own requests, so scripts, styles and module imports load. The page reloads once while that worker starts.

  • Renew without reloading. A link works for an hour. Before then, get a new one with GET …/preview-link?port=… and pass its token to the frame:

    frame.contentWindow.postMessage({ type: 'ocp-preview-token', token }, previewOrigin)

    The preview takes it from its embedders only (from its parent if none are set), and keeps working. It posts { type: 'ocp-preview-ready' } to its parent when it loads.

  • Your own tools in the page. preview_inject adds a script to every HTML page the preview serves, before </head>: { "script_url": "https://…" } or { "inline": "…" } (up to 16 KB). Use it for click-to-edit, "view as" or your own checks, without a proxy of your own. It's only added to previews, never to deployed apps. It runs under the page's own Content-Security-Policy with a nonce we add, so a strict policy doesn't block it.

  • Hot reload works through private previews in both modes. Tell your dev server the address it's reached at. For Vite: server: { allowedHosts: true, hmr: { clientPort: 443 } }; locally (pnpm sandbox:dev) use clientPort: 8788.

Change any of these later with PATCH, without restarting the sandbox.

Long-running processes

For dev servers and anything else that keeps running.

EndpointWhat it does
POST /api/v1/apps/{id}/sandboxes/{sandboxId}/processesStart one: { "name": "dev", "command": "pnpm dev --host 0.0.0.0 --port 5173" }. Replaces a process with the same name.
GET …/processes/{name}Whether it's running, its exit code, and the end of its output (log).
DELETE …/processes/{name}Stop it and everything it started.

Process names are lowercase letters, digits and dashes, up to 41 characters.

On this page