Skip to content

createVM

createVM starts a VM in the current browser tab and resolves once the VM is ready to use.

import.ts
import { createVM } from "@oxidevm/sdk";

Importing the package is safe during server rendering. Calling createVM needs a browser with module workers, so call it from client code only.

createVM
function createVM(options: SystemOptions): Promise<SystemVM>; // mode: "system"
function createVM(options: UserspaceOptions): Promise<UserspaceVM>; // mode: "userspace"
function createVM(options: CreateVMOptions): Promise<VM>; // either mode

The literal mode selects the overload, so TypeScript checks options and VM members that exist in only one mode. CreateVMOptions is SystemOptions | UserspaceOptions, and VM is SystemVM | UserspaceVM. The members of the returned VM are listed in VM.

A VM always runs in the mode you asked for. If that mode cannot start, createVM rejects with a VMError. It never starts the other mode instead.

Option Type Default Meaning
mode "userspace" | "system" required Userspace ("userspace") or System ("system").
workspaceId string none (ephemeral) Opens a durable workspace. See workspaceId.
image ImageRef<"userspace"> or ImageRef<"system"> the SDK’s pinned default image for the mode The signed image to run. See image.
imagePreparation "on-demand" | "before-ready" "on-demand" When image data is downloaded. See imagePreparation.
memoryMiB number the image’s recommendation Guest memory in MiB, an integer from 64 to 3072.
runtime RuntimeOptions { hosting: "self-hosted", baseUrl: "/oxidevm/runtime/" } Where the engine runs: in the hosted runtime frame, or from files your site serves. See runtime.
network NetworkOptions { mode: "local" } Guest internet access. See network.
preview PreviewOptions none The origin that serves previews of guest web servers. See preview.
signal AbortSignal none Cancels creation only. See Cancellation.
onProgress (progress: BootProgress) => void none Receives startup progress. See BootProgress.
restorePoint ManagedResume "none" Where the VM starts: cold, or from a restore point. See restorePoint.
shippedSnapshot string or ShippedSnapshotRef none A signed prepared start to open on a workspace’s first visit. See shippedSnapshot.
Option Type Default Meaning
initialUser "user" | "root" "user" Credentials of guest processes. See initialUser.
rootPersistence "ephemeral" | "persistent" "ephemeral" Whether writable system files are kept. See rootPersistence.
limits.maxProcesses number the image’s recommendation, else 32 Most live guest processes, an integer from 1 to 64. See limits.maxProcesses.
commandInterceptors "auto" | "off" or readonly CommandInterceptor[] "auto" How commands run: eligible JavaScript launches in browser workers, or every command in Linux. See commandInterceptors.
Option Type Default Meaning
vcpus 1 | 2 | 3 | 4 1 Virtual CPUs. Only 1 is accepted for now: 2, 3 and 4 fail with FEATURE_NOT_AVAILABLE and detail.reason: "release-gate-open". vm.capabilities.maximumVcpus reports 1.

Unknown option names, and options that belong to the other mode, fail with INVALID_ARGUMENT.

  • 1 to 128 characters: letters, digits, _ and -. Anything else fails with INVALID_ARGUMENT.
  • Omitted, the VM gets a new ephemeral workspace of its own, and vm.workspace.persistence is { kind: "ephemeral" }.
  • Present, the workspace is stored durably in this origin’s browser storage and kept between visits. See persistence.
  • A durable workspace is opened exclusively. While one VM holds a workspace ID, creating another VM with the same ID, in this tab or another, fails with STORAGE_ERROR and detail.reason: "workspace-in-use". Await dispose() before you reopen it, or move it between tabs with allowWorkspaceHandoff.

Pass an ImageRef whose mode matches mode; a reference of the other mode fails with INVALID_ARGUMENT. You get one from:

  • defineImage({ mode, manifestUrl, manifestSHA256, trust }), where trust is { algorithm: "Ed25519", keyId, publicKey } with the image author’s public key;
  • the module that oxidevm assets install --image <bundle> --reference <file> generates (see CLI);
  • defaultImage(mode, manifestUrl?), the SDK’s pinned default image, optionally served from another address. Its digest and key stay pinned whatever the address.

Before any guest code runs, the SDK verifies the whole manifest, which pins every file of the image, with the reference’s key; each file is checked against it when it is read. A durable workspace that already ran another image keeps running that image and names the requested one in vm.start.update; start with restorePoint: "update-image" to move it. vm.image always reports the image actually running.

Value Behavior
"on-demand" Image files are downloaded and verified when first needed.
"before-ready" The complete image is installed in durable browser storage before guest code runs. Later starts, saves and restores use local data only: missing or damaged data fails with STORAGE_ERROR and detail.reason: "image-not-cached" instead of downloading. Repair it with vm.prepareImage().

Installation reports { phase: "prepare-image" } progress.

RuntimeOptions
type RuntimeOptions =
| { hosting: "frame"; origin?: string | URL; license?: string }
| { hosting: "self-hosted"; baseUrl: string | URL; license?: string };
runtime.ts
const vm = await createVM({ mode: "userspace", runtime: { hosting: "frame" } });

{ hosting: "frame" } runs the engine in a hidden frame on the runtime origin, by default https://run.oxidevm.dev, which also serves the default Userspace image. Your page talks to it over a MessagePort. See Deploying.

  • origin is a bare https: origin, or http: on localhost or a loopback address, with no path, query or fragment.
  • The page must be cross-origin isolated; otherwise creation fails with FEATURE_NOT_AVAILABLE, detail.reason: "browser-unsupported" and detail.requirement: "cross-origin-isolation".
  • With the default origin and no preview option, the VM serves previews from https://<session>.oxvm.dev, as if you had passed preview: { origin: "https://oxvm.dev", isolation: "per-session" }.
  • Your own commandInterceptors workers fail with INVALID_ARGUMENT: the frame runs only the packaged interceptor.
  • A System VM in the frame needs an image that you host: the frame serves no System image. The frame downloads it, so serve it as Custom images describes.

{ hosting: "self-hosted", baseUrl } loads the engine release your site serves at baseUrl. See Self-hosting the engine.

  • baseUrl is an absolute https: URL, an http: URL on localhost or a loopback address, or an origin-relative path. It may not contain credentials, a query or a fragment. A trailing / is added when missing.
  • The SDK fetches each engine module from baseUrl and checks its size and SHA-256 against the engine release the SDK pins before any worker runs it, so moving the files cannot change which bytes are accepted. A missing or different file fails creation with CORE_LOAD_FAILED.
  • Without runtime, the SDK loads a self-hosted engine from /oxidevm/runtime/, where oxidevm assets install stages it for a project with assetBaseUrl: "/oxidevm/". See CLI.

license is a license token (oxl1. followed by two base64url parts) for your page’s origin, in either shape. A string of another form fails with INVALID_ARGUMENT.

  • Self-hosted, the token must also admit self-hosting. Without a token, creation fails with PERMISSION_DENIED and detail.reason: "license-required"; with one the engine does not accept, with detail.reason: "license-invalid". A page on localhost, a name ending in .localhost, a 127.0.0.0/8 address or [::1] needs no token. See Licenses.
  • Public use in the frame also requires a token. Missing tokens fail with PERMISSION_DENIED and detail.reason: "license-required". Rejected tokens fail with detail.reason: "license-invalid". Register and renew through Licenses.
NetworkOptions
type NetworkOptions =
| { mode: "local" }
| { mode: "relay"; connection: RelayConnectionOptions };
type RelayConnectionOptions =
| { kind: "personal"; workspaceName?: string }
| { kind: "server"; url: string | URL; sessionUrl?: string | URL; headers?: Record<string, string> };
  • { mode: "local" }: guest sockets stay inside the VM.
  • { mode: "relay", connection }: after the VM is ready and before createVM resolves, the SDK calls vm.network.connect(connection) once. If that fails or is cancelled, the VM is disposed and createVM rejects with the relay’s error (RELAY_ERROR, PERMISSION_DENIED, QUOTA_EXCEEDED or PROTOCOL_ERROR, with detail.reason). Invalid connection options fail with INVALID_ARGUMENT.
  • A personal relay asks the user to approve a six-digit pairing code, which creation cannot show. Create the VM with { mode: "local" }, follow vm.network.watch, and call vm.network.connect yourself. See Networking.
PreviewOptions
type PreviewOptions =
| { origin: string | URL; isolation: "per-session" }
| { origin: string | URL; isolation: "shared"; developmentOnly: true };
  • origin must be an origin only (no path, query or fragment), using https:, or http: on localhost or a loopback address.
  • "per-session" gives each session its own host under origin, for example https://<session>.preview.example.com, so cookies and storage stay separate between sessions.
  • "shared" serves every session from origin itself and requires developmentOnly: true. Sessions then share cookies and storage, so use it for local development only.
  • Without preview, vm.capabilities.previews is { available: false, reason: "not-configured" } and vm.exposePort fails with FEATURE_NOT_AVAILABLE. See Previews.
Value Start
"none" Cold boot from the workspace’s current files. This is the default.
"latest" Resume the newest restore point that can resume. A damaged or unreadable point gives way to the next older one; any other problem stops the search. If nothing resumes, Linux boots cold from the current files and vm.start reports reason and skipped. Required for shippedSnapshot.
"pre-resume" Cold boot from the files as they were before the last resume replaced them, which keeps files newer than that restore point. Without an earlier resume, the same as "none".
"fresh-image" Cold boot from the configured image’s own files. This resets the workspace: its saved files are not carried over.
"update-image" Move a durable workspace to the configured image, keeping the workspace’s own /workspace files. Use it after vm.start.update named a newer image.
{ id, createdAt } A SnapshotRef from vm.snapshots: resume that restore point. A reference to a restore point that does not exist fails with STORAGE_NOT_FOUND; a point that cannot resume boots cold, and vm.start reports reason and skipped.

Check vm.start.kind to learn which start happened. See Restore points.

A prepared start is a restore point signed by its author, built with oxidevm snapshot build or snapshot pack. It opens only when restorePoint is "latest" and the workspace has no restore point of its own; existing saves always take precedence.

Form Behavior
string The name of a prepared start that the image lists: at most 64 lowercase letters, digits, ., _ and -, starting with a letter or digit. If it cannot open, Linux boots cold and vm.start.shipped.reason says why.
ShippedSnapshotRef A reference from defineShippedSnapshot, usually inside the options module that oxidevm assets install --snapshot generates. It pins the prepared start’s manifest digest and sizes, the image it belongs to and the author key. If it cannot open, createVM rejects.

"user" runs guest processes with uid and gid 1000. "root" starts them with Linux root credentials; it requires commandInterceptors: "off", otherwise creation fails with INVALID_ARGUMENT.

"persistent" keeps the writable system paths /var, /etc, /srv and /usr/local between visits, separately from /workspace and the immutable image. They are scoped to the workspace and the exact image. /run, /tmp, sockets, devices and live kernel objects start fresh every time. "persistent" requires workspaceId (otherwise INVALID_ARGUMENT) and durable browser storage.

The most guest processes that may be alive at once, an integer from 1 to 64. Processes that your processes start count too. Without it, the limit recommended in the signed image applies, or 32 when the image has none.

"auto" runs eligible JavaScript launches in browser workers, for example the JavaScript program that bun run build starts in an image with an acceleration profile, and the packaged TypeScript interceptor runs tsc --noEmit in a browser worker when it matches the one installed TypeScript compiler it authenticates. The Linux side keeps their files, sockets, process identity, permissions and signals, and every other command runs as an ordinary Linux process.

Value Behavior
"auto" (default) Automatic acceleration and the packaged TypeScript interceptor.
"off" Every command runs as a Linux process: no acceleration and no interceptor. initialUser: "root" requires it.
[{ command: "tsc", worker }] Automatic acceleration with your own interceptor worker instead of the packaged one: exactly one entry, whose worker URL must be on the page’s origin. typescriptInterceptor() returns the packaged one. Your own worker needs a self-hosted engine; in the frame it fails with INVALID_ARGUMENT.

Acceleration, which "auto" and an interceptor array both turn on, needs all of the following, or creation fails:

  • a cross-origin isolated page (otherwise FEATURE_NOT_AVAILABLE);
  • with a self-hosted engine, a native-node-api.json descriptor beside the engine files at runtime.baseUrl, naming a separate execution origin that serves the SDK’s assets/native-process/ files (otherwise FEATURE_NOT_AVAILABLE); the hosted frame provides its own;
  • initialUser: "user" (otherwise INVALID_ARGUMENT).

While an accelerated JavaScript process runs, vm.snapshots.capture and vm.suspend fail with FEATURE_NOT_AVAILABLE and detail.reason: "js-process-active".

BootProgress
type BootProgress =
| { phase: "download"; asset: "engine" | "image" | "restore-point"; completedBytes: number; totalBytes: number | null }
| { phase: "validate" }
| { phase: "prepare-image"; stage: "download" | "verify" | "commit"; completedBytes: number; totalBytes: number }
| { phase: "boot"; mode: RuntimeMode }
| { phase: "restore"; step: "open"; completedBytes?: number; totalBytes?: number }
| { phase: "restore"; step: "wake" | "reconnect" }
| { phase: "ready" };
Phase Meaning
download Bytes of the engine modules, the image, or a prepared start’s restore point are arriving. totalBytes is null when the server sends no length.
validate The image manifest arrived and its signature is being checked.
prepare-image Image data is downloaded, verified and committed to browser storage, with byte progress per stage.
boot Linux starts cold. A cold start reports boot and no restore events.
restore A restore point is resumed: open (reading it, with byte progress), then wake (Linux continues where it left off), then reconnect (the guest is reconnected and its files brought up to date).
ready Sent once, right before createVM resolves.

The returned VM is ready to use:

  • the image has been authenticated and Linux has booted or resumed;
  • the workspace answers: Userspace has mounted /workspace, System has heard from its guest agent;
  • a network: { mode: "relay" } connection is established.

Workspace and process calls can follow immediately. vm.start reports whether the VM booted cold or resumed.

signal cancels creation only. Aborting it while createVM runs rolls the start back and rejects with ABORTED. After createVM has resolved, the signal no longer affects the VM: you own the VM and must call dispose().

open-vm.ts
import { createVM, VMError, type UserspaceVM } from "@oxidevm/sdk";
export async function openVM(signal: AbortSignal): Promise<UserspaceVM | undefined> {
try {
return await createVM({
mode: "userspace",
workspaceId: "my-project",
runtime: { hosting: "frame" },
signal,
onProgress: (progress) => {
console.log(progress.phase === "download"
? `${progress.asset}: ${progress.completedBytes} of ${progress.totalBytes ?? "?"} bytes`
: progress.phase);
},
});
} catch (error: unknown) {
if (error instanceof VMError && error.code === "ABORTED") return undefined;
throw error;
}
}

When creation fails or is cancelled, the SDK stops what it started, releases the workspace lease and frees downloaded assets before it rejects, so you can retry with the same workspaceId. If that cleanup fails as well, the rejection keeps the original code and message and adds detail.cleanupFailed: true and detail.cleanupError ({ code, message, detail? }).

Code Typical cause at creation
INVALID_ARGUMENT An unknown option, an option of the other mode, a value out of range, or an image reference of the other mode.
FEATURE_NOT_AVAILABLE No module workers, or a page that is not cross-origin isolated with the frame (both browser-unsupported), vcpus above 1 (release-gate-open), or Userspace commandInterceptors: "auto" without its requirements.
CORE_LOAD_FAILED Engine files at runtime.baseUrl are missing or differ from the pinned release, or the browser lacks a required WebAssembly feature.
PROTOCOL_ERROR The image manifest could not be downloaded or verified, or it is signed by another key (untrusted-key, image-trust-changed).
STORAGE_ERROR The workspace is open in another VM (workspace-in-use), or browser storage failed.
STORAGE_NOT_FOUND restorePoint names a restore point that does not exist.
PERMISSION_DENIED A public runtime without a license token that admits its origin (license-required, license-invalid).
RELAY_ERROR, PERMISSION_DENIED, QUOTA_EXCEEDED A network: { mode: "relay" } connection failed.
ABORTED signal was aborted.

Every code and detail.reason value is listed in Errors.