The Nullspace TypeScript SDK (@nullspace/sdk) gives you on-demand Linux machines from Node or the browser. Its public surface follows the same Python SDK primitives with TypeScript-friendly resource namespaces and camelCased fields. Generated OpenAPI types stay private; exported signatures use stable hand-written SDK types.

Install

npm install @nullspace/sdk
Requires Node 18+. WebSocket-backed surfaces (PTY, exec streaming, lifecycle events) need a global WebSocket, which Node 22+ and all browsers provide. On Node 18-20, install a WebSocket implementation such as ws and assign it to globalThis.WebSocket before using those streaming surfaces.

Configure

Authentication resolves from explicit options first, then the environment:
  • NULLSPACE_API_KEY — required API key.
  • NULLSPACE_API_URL (alias NULLSPACE_BASE_URL) — base URL.
import { NullspaceClient } from "@nullspace/sdk";

const client = new NullspaceClient({ apiKey: "ns_live_..." });
The TypeScript resolver is browser-safe. It reads explicit constructor options, then NULLSPACE_API_KEY, NULLSPACE_API_URL, and NULLSPACE_BASE_URL from the environment when process.env is available. It does not read .env, XDG config, or ~/.nullspace/config.json; use the Python CLI for saved local auth config.

Quickstart

Create → exec → file → destroy, in under 10 lines:
import { Machine } from "@nullspace/sdk";

await using machine = await Machine.create();

const result = await machine.commands.run("echo 'Hello!'", { shell: true });
console.log(result.stdout);

await machine.files.write("/hello.txt", "world");
console.log(await machine.files.read("/hello.txt"));
// `await using` destroys the machine when the scope exits.
For repeated calls, prefer a configured client so auth and base URL are shared:
import { NullspaceClient } from "@nullspace/sdk";

const client = new NullspaceClient({
  baseUrl: process.env.NULLSPACE_API_URL,
  apiKey: process.env.NULLSPACE_API_KEY,
});

const machine = await client.machines.create({ template: "base" });
const same = await client.machines.get(machine.id);
await client.machines.destroy(same.id);

Surface Map

SurfaceUse
Machine.create, Machine.connectOne-off machine create/connect with explicit or env auth.
client.machinesShared-client create, get, list, and destroy.
machine.commandsForeground, streaming, and background command/process control.
machine.filesMachine file read/write/list/move/remove/upload/download.
machine.gitClone, status, log, branch, checkout, add, commit, pull, and push inside a machine.
machine.ptyInteractive terminal sessions over WebSocket.
machine.codeStateful code interpreter runs and contexts.
machine.lifecyclePer-machine lifecycle history and live subscription.
client.snapshotsGet, list, delete, and resume snapshots.
client.volumesCreate, get, list, and delete shared volumes.
client.templatesBuild, list, get, and delete templates.
client.functionsInvoke deployed Function jobs/services and manage schedules/webhooks.

Machines

A Machine exposes namespaced resources, mirroring Python:
  • machine.commandsrun (with onStdout / onStderr streaming), list, kill, sendStdin, getLogs.
  • machine.filesread, write, list, exists, info, makeDir, remove, rename, upload / download with progress callbacks.
  • machine.ptyopen() returns a handle (send input, resize, async-iterate output, wait(), kill()).
  • machine.coderun() streams Jupyter-style execution events over SSE.
  • machine.lifecyclelist() history and subscribe() live events.
Create-time options include CPU, memory, disk, timeout, envs, metadata, cwd, volumes, internet access, typed egress policy, and idempotency keys.
import {
  Machine,
  egressCustom,
  type CreateMachineOptions,
} from "@nullspace/sdk";

const options: CreateMachineOptions = {
  template: "base",
  vcpus: 2,
  memoryMb: 1024,
  timeout: 600,
  cwd: "/workspace",
  envs: { APP_ENV: "dev" },
  internetAccess: true,
  egress: egressCustom({
    allowCidrs: ["0.0.0.0/0"],
    denyCidrs: ["169.254.169.254/32"],
  }),
};

await using machine = await Machine.create(options);
Runtime lifecycle and access helpers live on the machine:
const child = await machine.fork();            // copy-on-write fork
const snap = await machine.createSnapshot();   // durable snapshot
const resumed = await client.snapshots.resume(snap.id);
const preview = await machine.createSignedPreviewUrl(8080);
const lease = await machine.setTimeout(300, { timeoutAction: "hibernate" });
await machine.updateEgress(egressCustom({ allowCidrs: ["203.0.113.0/24"] }));
Use await using for short-lived work. Pass { keepAlive: true } as the second argument to Machine.create(...) or client.machines.create(...) when scope exit should leave the machine running.

Commands, Files, Git, PTY, Code

Commands can run in simple or streaming mode:
const result = await machine.commands.run("npm test", {
  cwd: "/workspace/app",
  shell: true,
  timeout: 120,
  onStdout: (chunk) => process.stdout.write(chunk),
  onStderr: (chunk) => process.stderr.write(chunk),
});

const processes = await machine.commands.list();
await machine.commands.kill(processes[0].pid);
Files support text, bytes, signed transfer URLs, uploads, and downloads:
await machine.files.write("/workspace/input.txt", "hello\n");
const text = await machine.files.read("/workspace/input.txt");
const exists = await machine.files.exists("/workspace/input.txt");
const listing = await machine.files.list("/workspace", { depth: 1 });

await machine.files.upload("/workspace/app.tar.gz", appTarballBytes, {
  progress: (event) => console.log(event.bytesCompleted, event.bytesTotal),
});
const resultBytes = await machine.files.download("/workspace/result.json");
Git and PTY are machine-scoped:
await machine.git.clone("https://github.com/acme/repo", "/workspace/repo");
const status = await machine.git.status("/workspace/repo");
await machine.git.commit("update generated files", "/workspace/repo");

await using pty = await machine.pty.open({ cwd: "/workspace/repo" });
await pty.send("ls\n");
for await (const chunk of pty.output()) {
  process.stdout.write(Buffer.from(chunk).toString());
}
Code interpreter runs return structured outputs and context state:
const execution = await machine.code.run("x = 1 + 1\nx", {
  language: "python",
});
console.log(execution.results, execution.error);

const context = await machine.code.createContext({ language: "python" });
await machine.code.run("import pandas as pd", { context: context.id });

Volumes, Snapshots, Templates

Volumes are metadata-managed through client.volumes and mounted through a machine. Direct volume file operations are currently covered by the Python SDK, CLI, and HTTP API.
import type { VolumeMount } from "@nullspace/sdk";

const vol = await client.volumes.create("data");
const mount: VolumeMount = { ref: vol.id, mountPath: "/data" };
const mounted = await machine.attachVolume(mount.ref, mount.mountPath);
const attachments = await machine.listVolumes();
if (mounted.id !== null) {
  await machine.detachVolume(mounted.id);
}
Snapshots are split between machine actions and the client snapshot resource:
const snapshot = await machine.createSnapshot();
const restored = await client.snapshots.resume(snapshot.id);
for (const item of await client.snapshots.list({ machineId: machine.id })) {
  console.log(item.id, item.createdAt);
}
await client.snapshots.delete(snapshot.id);
Templates use a fluent builder and stream structured build log entries:
const template = await client.templates
  .builder()
  .fromUbuntuImage("22.04")
  .aptInstall(["curl", "git"])
  .pipInstall(["pytest"])
  .workdir("/workspace")
  .skipCache()
  .build({
    name: "team/reviewer",
    tags: ["stable"],
    onLogEntry: (entry) => console.log(entry.level, entry.message),
  });

Functions

The SDK invokes deployed Functions; authoring and deploy use the shared function.py descriptor plus the Python-owned CLI.
import { NullspaceClient } from "@nullspace/sdk";

const client = new NullspaceClient();
const fn = client.functions.get("typescript-minimal-job", {
  environment: "staging",
});

const run = await fn.run({ input: { subject: "functions" } });
const result = await run.wait();

console.log(result.status, await run.outputs());

const call = await fn.spawn({ input: { subject: "queued" } });
console.log(call.status, call.queuedReason, call.retryAfterSeconds);
Schedules, webhooks, logs, outputs, and service URLs are available from the same handle:
const schedule = await fn.createSchedule({
  kind: "cron",
  expression: "0 8 * * 1",
  timezone: "UTC",
  route: "/report",
});

await fn.runSchedule(schedule.id);
const webhook = await fn.createWebhook({
  signingSecret: process.env.WEBHOOK_SECRET!,
  route: "/review",
});
const service = await fn.serviceUrl();
for await (const event of run.logs({ follow: true })) {
  console.log(event.level, event.message);
}
Deploy TypeScript projects with the CLI:
cd examples/typescript/functions/minimal_job
NULLSPACE_ENVIRONMENT=staging nullspace deploy --json
Use max_concurrency in function.py to cap concurrent job runs. Sync run/invoke calls return 429 capacity_exceeded with a Retry-After hint when saturated; detached spawn calls expose queuedReason and retryAfterSeconds when admitted to the bounded backlog. Function deploy, rollback, checkpoint/branch, retained-machine shell access, proxy-auth token management, named secrets, and environments are CLI/Python management surfaces today.

Exported Types

Use the exported public types instead of generated OpenAPI types:
import type {
  CommandResult,
  FileInfo,
  FunctionRunInfo,
  HostInfo,
  LifecycleEvent,
  MachineInfo,
  MachineMetrics,
  PreviewUrl,
  SnapshotInfo,
  TemplateInfo,
  VolumeAttachment,
  VolumeInfo,
  VolumeMount,
} from "@nullspace/sdk";
Common option types include CreateMachineOptions, RunOptions, FileOptions, PtyOpenOptions, RunCodeOptions, BuildOptions, and the Function option types exported from @nullspace/sdk.

Errors

Every failure is a typed subclass of MachineError with a stable code, matching the Python hierarchy: NotFoundError, AuthError, ConflictError, QuotaExceededError, RateLimitError, TimeoutError, BuildError, FileUploadError, and more.
import { NotFoundError } from "@nullspace/sdk";

try {
  await client.machines.get("mch_missing");
} catch (err) {
  if (err instanceof NotFoundError) {
    // ...
  }
}

Browser support

The REST, file transfer, and SSE core work in browsers; WebSocket-backed surfaces (PTY, exec streaming, lifecycle) need a global WebSocket. WebSocket routes authenticate with the nullspace-api-key subprotocol so browsers work without setting headers. Use signed transfer URLs when browser code should move file bytes directly.

Not included

The MCP server and CLI remain Python-owned (nullspace-sdk[cli,mcp]). Some advanced REST domains the Python SDK exposes are not yet wrapped in the TypeScript surface; the per-operation coverage manifest lives in specs/client-surfaces/typescript-sdk.yaml.