Move your E2B JavaScript SDK v2 integration to Freestyle with the mappings
below. Use exec for commands and Git, PTY sessions for streaming or
long-running work, and snapshots for reusable environments.
For notebook and desktop workloads, see Jupyter and GNOME.
Why Migrate
Freestyle provides full Linux VMs for workspaces that live for hours or weeks. A VM can keep running without a timeout heartbeat, pause with its memory intact, and resume the same processes later.
- Reusable running environments. Snapshots capture memory and disk, including a dev server that is already listening.
- A complete Linux machine. Ubuntu includes systemd, Docker, Node.js, Bun, Python, and Git. Nested virtualization, FUSE, eBPF, and WireGuard are available for workloads that need them.
- Control over resources and networking. Resize a VM as it grows, isolate tenants with VPCs, and use TLS rules to hold provider credentials at the edge.
See base snapshots for image contents and sizes, and pricing and limits for plan capacity.
Map Your Current Setup
Map each call to the operation you need. Commands and Linux tools run inside the VM; files, sessions, snapshots, and networking have SDK APIs.
| E2B | Freestyle |
|---|---|
Sandbox.create(template, opts) | freestyle.vms.create({ snapshotId, firewall, ... }) |
Sandbox.connect(sandboxId) | freestyle.vms.ref(vmId); commands and file writes wake a paused VM, file reads leave it paused |
sandbox.sandboxId | vmId, or the slug you chose |
metadata, Sandbox.list({ query }) | metadata, freestyle.vms.list({ metadata, state }) |
sandbox.getInfo(), isRunning() | vm.data() and its state |
timeoutMs, setTimeout() | Choose a lifecycle policy; extend deletion with vm.update({ ttlSeconds }) |
lifecycle: { onTimeout: "pause" } | maxRunSeconds for a run budget, or vm.pause() when your application lease expires |
sandbox.pause() | vm.pause() |
sandbox.kill() | vm.delete() |
sandbox.fork(), createSnapshot() | vm.snapshot(), then vms.create({ snapshotId }) |
commands.run(cmd, { cwd, envs, user, timeoutMs }) | vm.exec({ command, env, linuxUser, timeoutMs }) |
commands.run(cmd, { background, onStdout }) | vm.pty.open({ exec: cmd, onData, onExit }) |
commands.connect(pid), kill(pid) | vm.pty.attach({ session }), vm.pty.close() |
pty.create(), sendInput(), resize(), kill() | vm.pty.open(), session.write(), resize(), pty.close() |
files.read(), write(), list(), getInfo() | vm.fs.readTextFile(), writeTextFile(), readDir(), stat() |
files.read(path, { format: "bytes" }), write(path, bytes) | vm.fs.readFile() (a Uint8Array, not a Buffer), writeFile() |
files.write([...entries]) | Promise.all of writeFile calls |
files.read(path, { format: "stream" }) | vm.fs.readFileStream() |
files.rename() | vm.exec("mv ...") |
files.watchDir() | inotifywait in a PTY session |
uploadUrl(), downloadUrl() | Your server authorizes and streams transfers through vm.fs |
| Git operations | vm.exec("git ..."); private Git credentials can stay at the edge |
sandbox.getHost(port) | A TLS rule from a style.dev name or your domain to the port |
allowPublicTraffic: false, traffic token | Forward auth on the ingress rule |
allowInternetAccess, allowOut | Firewall rules; egress is denied until allowed |
envs at create | env on each exec, or bake into the snapshot |
envd access token for clients | Identity tokens |
Template().fromImage().runCmd()..., Template.build() | Boot a base snapshot, set it up, vm.snapshot() |
setStartCmd(cmd, readyCmd) | Start a systemd unit, wait for it, snapshot while running |
template:tag | Snapshot slug |
| Secrets injected into requests | TLS transforms |
Create, Find, And Reconnect
Install freestyle and set FREESTYLE_API_KEY in your backend. Create a
workspace with a snapshot, an explicit firewall, and a slug you choose:
import { Freestyle } from "freestyle";
const freestyle = new Freestyle();
const { vm, vmId } = await freestyle.vms.create({
snapshotId: "my-template", // omit for the Ubuntu base snapshot
slug: `workspace-${workspaceId}`,
metadata: { workspaceId, tenant: "acme" },
firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
});
Reconnect with a handle by ID or slug. vms.ref() makes no network call:
const workspace = freestyle.vms.ref(`workspace-${workspaceId}`);
const result = await workspace.exec("uptime");
const info = await workspace.data();
const { vms } = await freestyle.vms.list({ metadata: `workspaceId:${workspaceId}` });
Commands and file writes wake a paused VM. File reads and directory
listings leave it paused. data().state
reports starting, running, pausing, paused, or stopped; a missing
VM returns a FreestyleApiError with status 404.
Use vm.pause() to preserve the workspace and vm.start() to resume it.
vm.delete() permanently removes the VM and the firewall and TLS rules
that name it.
Replace The Timeout Heartbeat
Freestyle has no default running-time deadline. Keep a VM running until your application pauses or deletes it, or configure a lifecycle policy:
| Need | Freestyle |
|---|---|
| Pause after network inactivity | idleTimeoutSeconds |
| Pause after a fixed duration of one run | maxRunSeconds |
| Limit total running time across resumes | maxRunTotalSeconds |
| Delete at a deadline | ttlSeconds, measured from createdAt and extendable with vm.update() |
| Delete after time spent paused or stopped | autoDeleteSeconds |
| Pause when an application lease expires | Call vm.pause() from your backend when the lease expires |
The lifecycle guide shows how to set and extend a deadline. TTL deletion is asynchronous, so use it for lifetime cleanup rather than exact-time scheduling.
Idle time measures network activity. PTY output and CPU work alone do not
reset it; leave idleTimeoutSeconds unset for a long-running job that must
finish without user input. Some plans cap how long an unused VM is kept;
vm.data().autoDeleteSeconds reports that policy.
Run Commands
commands.run() in the foreground becomes vm.exec(). The options rename:
envs is env, user is linuxUser, and cwd is a cd inside the command
string, which the guest runs through its shell. Two behaviors differ:
- Exit status is a value, not an exception. E2B throws
CommandExitErroron a non-zero exit. Freestyle returns{ stdout, stderr, statusCode }and leaves the check to you.statusCodeisnullwhen the timeout killed the command. - The timeout is capped at five minutes.
timeoutMsaccepts up to 300000. Use a PTY for longer commands or streaming output.
// commands.run("npm test", { cwd: "/project/workspace", envs: { CI: "1" }, user: "node", timeoutMs: 120_000 })
const result = await vm.exec({
command: "cd /project/workspace && npm test",
env: { CI: "1" },
linuxUser: "node",
timeoutMs: 120_000,
});
if (result.statusCode !== 0) {
throw new Error(`tests failed: ${result.stderr}`);
}
exec starts in the user’s home directory and runs as the VM’s default user,
the account holding uid 1000, unless linuxUser names another. On the Ubuntu
base snapshots that is ubuntu, with passwordless sudo; E2B’s default was
user. If your image relies on a node account, create it in the snapshot
and name it on every call, or use vm.linuxUser("node") once and call exec
and pty on that handle.
stdin supplies up to 1 MiB as a base64 string. For interactive input, use
a PTY and session.write().
Stream And Detach Long Commands
Use vm.pty.open({ exec }) for installs, builds, agents, and other
long-running commands. It streams output through onData, reports the exit
code through onExit, and keeps running when the client disconnects.
Name the session so another client can attach to the same process:
const build = await vm.pty.open({
exec: "cd /project/workspace && npm run build",
slug: "build-42",
onData: (bytes) => process.stdout.write(bytes),
onExit: (code) => console.log("build exited", code),
});
build.detach(); // disconnect; the build keeps running
const attached = await vm.pty.attach({
session: "build-42",
onData: (bytes) => process.stdout.write(bytes),
onExit: (code) => console.log("build exited", code),
});
attached.signal("sigint"); // request cancellation
await vm.pty.close("build-42"); // kill and remove the session
PTY output combines stdout and stderr as terminal output. A named open()
is get-or-create: an existing session is reused without rerunning exec.
Use a new slug for a new job. PTY commands have no five-minute exec limit;
the VM’s own lifecycle policy still applies.
For a supervised service that should restart after exiting, use systemd. See PTY sessions for input, signals, and reattachment.
Move Files
Use vm.fs for text, bytes, directory listings, and file metadata:
const text = await vm.fs.readTextFile("/project/workspace/package.json");
const bytes = await vm.fs.readFile("/project/workspace/logo.png"); // Uint8Array
await vm.fs.writeTextFile("/project/workspace/.env", "PORT=3000\n");
await vm.fs.writeFile("/project/workspace/data.bin", bytes, { mode: 0o644 });
const entries = await vm.fs.readDir("/project/workspace");
const info = await vm.fs.stat("/project/workspace/package.json");
await vm.fs.mkdir("/project/workspace/tmp");
await vm.fs.remove("/project/workspace/tmp");
Use Promise.all for batch writes and vm.exec("mv old new") for renames.
For recursive copies, use freestyle vm scp.
For large transfers, upload a Blob with writeFile() and download with
readFileStream(). The SDK streams and resumes chunked uploads; reads
support offset and length. Files can be up to 16 GiB.
Authorize client uploads and downloads in your backend and stream them
through vm.fs with the account API key. Identity tokens
cover exec and PTY; they do not grant the root-level file API.
New files written through vm.fs are root-owned. Set ownership for your
application user after uploading. See Files for streaming,
permissions, and paused-VM reads.
Watch A Directory
Run inotifywait in a PTY session. Install inotify-tools in your snapshot,
then stream the events from each workspace:
await vm.exec("sudo apt-get update && sudo apt-get install -y inotify-tools");
const watcher = await vm.pty.open({
exec: "inotifywait -m -r -e create,delete,modify,moved_from,moved_to,attrib /project/workspace",
slug: "watch-workspace",
onData: (bytes) => process.stdout.write(bytes),
});
Reattach by session name after disconnecting; vm.pty.close("watch-workspace")
stops the watcher. If you parse events, buffer terminal output across chunks
until each line is complete. Output alone does not reset a VM idle timeout.
Terminals
Open a shell without exec, then relay its bytes to your terminal UI:
const session = await vm.pty.open({
cols: 120,
rows: 30,
slug: "main",
onData: (bytes) => socket.send(bytes),
});
session.write("ls -la\n");
session.resize({ cols: 200, rows: 60 });
session.signal("sigint");
session.detach(); // leave the shell running
Reattach with vm.pty.attach({ session: "main", onData }); close the process
with vm.pty.close("main"). Select a user with vm.linuxUser("node").pty
after creating that user in your snapshot.
Sessions survive disconnects and pause/resume. Start a paused VM before
reattaching; the SDK requires an explicit attach() after a connection drops.
For browsers, relay the authenticated WebSocket through your backend as in
Run a Web Terminal.
Rebuild Templates As Snapshots
Translate your E2B template’s setup steps into a shell script and run it on an Ubuntu VM. Node.js, Bun, Python, Git, and Docker are already installed; check the base image inventory before adding packages.
The CLI runs the script, streams its output, and takes a snapshot only after it exits successfully:
freestyle snapshot create --base freestyle/ubuntu --script ./setup.sh --slug my-template
Create workspaces from that snapshot:
const { vm } = await freestyle.vms.create({
snapshotId: "my-template",
firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] },
});
| Template step | Snapshot setup |
|---|---|
runCmd | A command in setup.sh, or vm.exec(); use a PTY for long-running steps |
copy | vm.fs.writeFile() or freestyle vm scp |
setUser, setWorkdir | Create the Linux user, set ownership, then select linuxUser and cd in commands |
setEnvs | Pass env to commands or set Environment= in a systemd unit |
setStartCmd(cmd, readyCmd) | Start a systemd unit and wait for readiness before capturing the snapshot |
fromDockerfile | Build and run your Docker image inside the VM |
| Names and tags | Give the snapshot a slug; keep its immutable ID when you need a pinned version |
A snapshot captures running processes as well as files, so a dev server
started before capture is already running in each new workspace. For an SDK
build, call vm.snapshot({ slug: "my-template" }) after setup completes.
See snapshot builds and the
Next.js guide for complete examples.
Publish Previews
sandbox.getHost(port) returns a hostname that exists for every port. On
Freestyle you declare which ports are public: one TLS rule routes one
hostname to one VM port. Any unused subdomain of style.dev is free and needs
no DNS, or use a domain you have verified.
// const url = `https://${sandbox.getHost(3000)}`
const domain = `ws-${workspaceId}.style.dev`;
await freestyle.tls.rules.create({
action: "allow",
domain,
source: { public: true },
destination: { vmId, port: 3000 },
});
const url = `https://${domain}`;
The edge terminates HTTPS and forwards the connection to the guest port, so
the dev server must listen on 0.0.0.0. Rules are deleted with the VM. A
public preview request wakes a paused VM. With forward auth, the check runs
first: denied requests and authorization failures leave the VM paused;
allowed requests can wake it and resume compute billing.
Private previews. allowPublicTraffic: false plus the
e2b-traffic-access-token header becomes forward auth.
Freestyle calls your authorization endpoint for each request with the
caller’s Authorization and Cookie headers and forwards only on a 2xx,
so the check can use your existing session system. Reuse one authorizer
across all your preview routes:
const auth = await freestyle.tls.forwardAuth.create({
url: "https://api.acme.com/previews/authorize",
headers: { authorization: `Bearer ${process.env.PREVIEW_AUTH_SECRET}` },
timeoutMs: 1500,
protectedCookies: ["__Host-acme-session"], // seen by your authorizer, never by the VM
});
await freestyle.tls.rules.create({
action: "allow",
domain,
source: { public: true },
destination: { vmId, port: 3000 },
forwardAuth: { id: auth.id },
});
Your authorizer reads X-Freestyle-TLS-Rule-Id to learn which preview is
being requested and returns 2xx to allow, 3xx to send a signed-out user to
login, or 4xx to refuse. WebSocket upgrades pass through the same rule, so
hot module reloading works behind it. A cookie-based sign-in handoff for many
previews on one authorizer is in
Protect A Sandbox With Forward Auth.
Hand Clients Scoped Access
E2B lets a backend hand its desktop or CLI clients the sandbox ID, the envd
access token, and the traffic token so they can call the sandbox directly
without the team API key. Freestyle’s equivalent is an identity: your server
grants it one or more VMs, restricted to named Linux users, and mints a token
the client uses as its own credential.
const { identity, identityId } = await freestyle.identities.create();
await identity.permissions.vm.grant({ vmId, allowedLinuxUsers: ["node"] });
const { id: tokenId, token } = await identity.tokens.create();
return { vmId, token }; // to the desktop app or CLI
import { Freestyle } from "freestyle";
const freestyle = new Freestyle({ identityAccessToken: token });
const workspace = freestyle.vms.ref(vmId).linuxUser("node");
await workspace.exec("git status");
const shell = await workspace.pty.open({ onData: render });
The grant carries allowedLinuxUsers, so a client holding it cannot run
anything as root, and a call that names no user is refused. It also cannot
touch VMs it was not granted or create account resources. Revoke a client
with identity.tokens.revoke(tokenId) or identity.permissions.vm.revoke(vmId)
without rotating anything else, and delete the identity when the user leaves.
SSH accepts the same token, so ssh <vm>+node:<token>@beta-ssh.freestyle.sh
gives a CLI a terminal with no code at all.
An identity token reaches only the exec and PTY routes. It cannot call
vm.fs, which runs as root in the guest, so an E2B client that read and
wrote files directly with its scoped session needs one of two changes. Route
file traffic through your server, which holds the API key and can check the
user’s permission on each request. Or move small transfers onto exec as the
granted user, with stdin for uploads of up to 1 MiB and stdout for
downloads, which keeps every byte under that user’s own permissions.
Control Egress
An E2B sandbox has Internet access unless allowInternetAccess: false, and
allowOut and denyOut refine that. A Freestyle VM has none until a
firewall rule allows it, and only allow rules exist.
| E2B | Freestyle |
|---|---|
allowInternetAccess: true (default) | firewall: { rules: [{ action: "allow", source: {}, destination: { public: true } }] } |
allowInternetAccess: false | firewall: { rules: [] } |
allowOut: ["1.1.1.1", "8.8.8.0/24"] with denyOut: allTraffic | One rule per range: destination: { cidr: "8.8.8.0/24" } |
allowOut: ["api.github.com"] | A TLS rule per exact domain, destination: { public: true } |
allowOut: ["*.example.com"] | Not available yet; only exact names steer |
denyOut: [...] alongside broad allow | Not available; start from nothing and allow what is needed |
| Secrets injected into requests | A transform on the TLS rule |
A TLS egress rule does two things at once: it admits the VM to that domain with no broader Internet grant, and it can inject a header the guest never held. That is how you replace E2B secrets with a key that lives at the edge:
await freestyle.tls.rules.create({
action: "allow",
domain: "api.openai.com",
source: { vmId },
destination: { public: true },
transform: [{ headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}` } }],
});
The OpenAI, Anthropic, and private Git guides cover the client-side certificate trust each stack needs.
Other Workflows
| Workflow | Use inside the VM |
|---|---|
| Git | vm.exec("git ...") for clone, status, diff, commit, and other Git commands; use a PTY for long transfers |
| Many forks | Snapshot once, then create multiple VMs from that snapshot |
| Resource usage | vm.data().cpuTimeSeconds for CPU time; free -b and df -B1 through exec for current guest memory and disk usage |
| Object storage | Mount a bucket with standard Linux tools |
| MCP servers or notebooks | Run the server or Jupyter in the VM and route a domain to it |
For lifecycle state, read vm.data() or vms.list({ state }). For a fresh
boot, stop the VM and start it again; pause()
preserves memory.
Verify Before Cutting Over
Run your application’s existing integration tests against a workspace from your snapshot. Check command exit handling, PTY reattachment, file ownership, pause/resume, and preview authorization with WebSocket traffic. Test scoped clients with only their identity token. Then route new workspaces to Freestyle and drain the existing E2B workspaces.