# How to build a browser-based online IDE on cloud sandboxes Back each workspace with a sandbox. Use the files API for the editor, a terminal over WebSocket, live file events and an app preview. **On Runtime, an online IDE workspace used 40 hours a month costs about $1.60** on 2 vCPU and 4 GiB, at the rates in effect on 26 October 2026. The sandbox is billed for the CPU that builds and servers use. It pauses when the browser tab goes quiet. Each workspace is a full Ubuntu 24.04 machine with `sudo`, Python 3.12, Node.js 24, Bun, git and a C compiler. It wakes when the user comes back. ## What each IDE feature calls Your front end draws the editor. Your back end holds the API key and makes these calls on the user's behalf: | IDE feature | Runtime call | | -------------------------- | ----------------------------------------------------------------------------------------- | | Open a workspace | `Sandbox.getOrCreate(name, { idlePauseSeconds })` | | File tree | `files.list(path, { depth })`: type, size and path of each entry | | Open and save a file | `files.readText(path)`, and `files.write(path, text)`, which replaces the file atomically | | Rename, delete, new folder | `files.rename`, `files.remove`, `files.mkdir` | | Quick open | `files.glob("**/*.ts")` | | Search in files | `exec(["rg", "--json", query])`: ripgrep is in the image | | Tree kept in sync | `files.watch` reports create, write, remove, rename and chmod events as they happen | | Terminal | `terminal({ cols, rows, onData })`, with `write` and `resize` | | Run and preview | Start the dev server with `spawn`, then call `previews.create(port)` | | Upload and download | `files.upload` and `files.download` move whole folders as one archive | ## The short answer: a workspace and its terminal ```ts check import { Sandbox } from "withruntime"; export async function openWorkspace(userId: string, onTerminalBytes: (b: Uint8Array) => void) { const sbx = await Sandbox.getOrCreate(`ide-${userId}`, { diskMiB: 16_384, idlePauseSeconds: 1200, // pause 20 minutes after the last request labels: { kind: "ide", user: userId }, }); const tree = await sbx.files.list("/workspace", { depth: 2 }); const term = await sbx.terminal({ cols: 120, rows: 32, onData: onTerminalBytes }); const watch = await sbx.files.watch( "/workspace", (event) => console.log(event.type, event.path), // push to the browser to refresh the tree { recursive: true, exclude: ["node_modules", ".git/**"] }, ); return { sbx, tree, term, watch }; } const ws = await openWorkspace("u-7", (bytes) => process.stdout.write(bytes)); await ws.sbx.files.write("/workspace/hello.py", "print('saved from the editor')\n"); ws.term.write("python3 hello.py\n"); // keystrokes from xterm.js go here ws.term.resize(100, 30); // when the browser pane changes size ``` ```python check from withruntime import Sandbox def open_workspace(user_id: str): sbx = Sandbox.get_or_create( f"ide-{user_id}", disk_mib=16_384, idle_pause_seconds=1200, # pause 20 minutes after the last request labels={"kind": "ide", "user": user_id}, ) tree = sbx.files.list("/workspace", depth=2) term = sbx.terminal(cols=120, rows=32) watch = sbx.files.watch("/workspace", recursive=True, exclude=["node_modules", ".git/**"]) return sbx, tree, term, watch sbx, tree, term, watch = open_workspace("u-7") sbx.files.write("/workspace/hello.py", "print('saved from the editor')\n") term.write("python3 hello.py\n") term.write("exit\n") while (chunk := term.recv()) is not None: print(chunk.decode(errors="replace"), end="") ``` The terminal is a real terminal, with colors. An in-browser terminal component can pass its bytes straight through. A watch reports changes made by the terminal, a build or an agent, not only by the editor's own saves. So the file tree stays correct. A sandbox runs up to four watches. Each watch lasts an hour by default. When a sandbox pauses, a watch ends with the reason `paused`. Calling `resume()` after the wake continues with nothing lost. ## Run the user's app ```ts check import { Sandbox } from "withruntime"; const sbx = await Sandbox.getOrCreate("ide-u-7", { idlePauseSeconds: 1200 }); const server = await sbx.spawn("npm run dev -- --port 5173", { cwd: "/workspace/app" }); const preview = await sbx.previews.create(5173); console.log(server.id, preview.urlWithToken); // open in the IDE's preview pane ``` A preview is private unless you make it public. It supports WebSockets for hot reload. On the next visit, the preview wakes a paused workspace. While the workspace wakes, the visitor sees a short "Waking up" page. Preview addresses are under `runtimehost.com`. So the user's app never shares an origin with your IDE. ## Or run VS Code itself in the sandbox There are two ways to give users the full VS Code instead of your own editor: - **Their desktop VS Code:** run `runtime sandboxes ssh config --install` once on their machine. Then connect with Remote-SSH to `ide-u-7.runtime`. The connection goes through Runtime's API with their key. The sandbox opens no port ([SSH and editors](/docs/editors)). - **In the browser:** install code-server, the open-source VS Code for the browser. Share its port through a private preview. The preview token is then the only way in. ```bash no-run curl -fsSL https://code-server.dev/install.sh | sh code-server --bind-addr 127.0.0.1:8080 --auth none # start it with runtime sandboxes spawn runtime sandboxes preview ide-u-7 8080 # a private HTTPS address for it ``` With `--auth none`, keep the preview private. The preview token replaces code-server's password. ## What a hosted IDE needs | Need | How Runtime covers it | | ------------------------------- | ------------------------------------------------------------------------------- | | A machine per user | A Firecracker microVM with its own kernel for each workspace | | Work kept overnight | Pausing keeps files, memory and running processes while you have credit | | The project's toolchain | A custom image from its own Dockerfile | | Containers in the workspace | `sudo enable-docker` for Docker Engine and Compose | | Databases and debuggers locally | `runtime sandboxes port-forward ` | | Teams | Owner, admin, developer and billing roles on one account ([teams](/docs/teams)) | | Big repositories | Disk size set with `diskMiB`; uploads in checked, resumable chunks | ## What it costs Take 300 workspaces. Each is open 40 hours a month on 2 vCPU and 4 GiB. Builds and the dev server use 0.25 of a vCPU on average. Each workspace is paused for the other 680 hours of a 30-day month, with 2 GB of its own disk and memory stored: ``` CPU: 300 × 40 h × 0.25 vCPU × $0.025 = $75.00 Memory: 300 × 40 h × 4 GiB × $0.0075 = $360.00 Paused: 300 × 2 GB × $0.08 × 680 h / 720 h = $45.33 Total: $480.33 ``` That is $1.60 per workspace per month. Paused storage is charged on the blocks that belong only to the workspace. Usage is measured when the workspace pauses ([pricing](/docs/pricing#paused-storage)). ## Start ```bash no-run npx withruntime sandboxes create --name ide-demo --get-or-create --idle-pause 1200 npx withruntime sandboxes shell ide-demo ``` The first command asks you to approve the connection in your browser. The second command opens the same terminal that your IDE shows. Related: [per-user dev environments](/use-cases/per-user-dev-environments), [coding playground](/use-cases/coding-playground), [app builder platform](/use-cases/app-builder-platform), [run Docker in a sandbox](/how-to/run-docker-in-a-sandbox). ## Sources - [code-server install](https://coder.com/docs/code-server/install): the install script, read 26 October 2026. - [code-server FAQ](https://coder.com/docs/code-server/FAQ): `--bind-addr` and `--auth none`, read 26 October 2026. Facts on this page were checked on 26 October 2026.