code-runner
Guides

Integrate from your backend

Submit jobs, drive stdin, and sign channel auth with the Node SDK.

Your backend is the trusted consumer of code-runner — it holds the EXECUTOR_API_TOKEN and the SOKETI_APP_SECRET, and it's the only thing that talks to the API directly. The @teovilla/code-runner-sdk-node package gives you a typed client plus zero-dependency channel-auth signing.

This SDK is server-side only. It carries secrets — never bundle it into a browser. For the browser, use @teovilla/code-runner-react.

Install

pnpm add @teovilla/code-runner-sdk-node
npm install @teovilla/code-runner-sdk-node
yarn add @teovilla/code-runner-sdk-node

Requires Node 22+. The package re-exports the relevant wire-contract types, so you don't need to install the contract separately.

Create a client

import { CodeRunnerClient } from '@teovilla/code-runner-sdk-node';

const client = new CodeRunnerClient({
  baseUrl: process.env.CODE_RUNNER_URL!, // e.g. "http://localhost:8080"
  token: process.env.EXECUTOR_API_TOKEN!, // the bearer token
});
OptionTypeNotes
baseUrlstringThe gateway base URL.
tokenstringEXECUTOR_API_TOKEN; sent as Authorization: Bearer ….
fetchFetchLike?Optional custom fetch; defaults to globalThis.fetch.

Client methods

MethodEndpointDescription
listLanguages()GET /v1/languagesList available languages & versions.
execute(req)POST /v1/executeEnqueue a job (raw passthrough) → { jobId, channel, status }.
executeFiles({ language, version?, files })POST /v1/executeConvenience submit: accepts text files, binary Buffers, and raw FileInputs, routing each inline-vs-CAS automatically.
blobs.upload(buffer, { ttlSeconds? })blob handshakeUpload a buffer to the blob store{ ref } (skips the PUT if already present).
blobs.check(hashes)POST /v1/blobs/checkLow-level existence check passthrough.
getJob(id)GET /v1/jobs/:idPoll job status.
getOutput(id)GET /v1/jobs/:id/outputPull the persisted RunResult (needs collectOutput). For compiled languages it includes an optional compile block with the build logs, distinct from the run stdout/stderr.
start(id)POST /v1/jobs/:id/startSend the start signal (the handshake).
sendStdin(id, chunk)POST /v1/jobs/:id/stdinWrite a chunk to the process stdin.
closeStdin(id)POST /v1/jobs/:id/stdin/closeSignal EOF (Ctrl-D).
kill(id)POST /v1/jobs/:id/killSIGKILL the sandbox.

Submit a job

const job = await client.execute({
  language: 'python',
  files: [
    { name: 'main.py', content: "name = input('name? ')\nprint(f'hello {name}')\n" },
  ],
  // optional per-request overrides; absent fields use the manifest defaults
  limits: { wallTimeMs: 30000, memoryMb: 128 },
});

console.log(job.jobId);  // "550e8400-..."
console.log(job.channel); // "private-run-550e8400-..."

execute returns before the process starts. The next step is the start-handshake: your client subscribes to job.channel over soketi, and then you call client.start(job.jobId).

Submit multiple files & binary input

executeFiles is the convenience wrapper for richer inputs. Pass text files as { name, content }, binary files as { name, data } (a Buffer/Uint8Array — the SDK base64-encodes them for you), and raw wire FileInputs if you prefer. A name may contain / for subdirectories:

import { readFileSync } from 'node:fs';

const job = await client.executeFiles({
  language: 'python',
  files: [
    // text + a CSV in a subdirectory
    { name: 'main.py', content: "import pandas as pd; print(pd.read_csv('data/input.csv').shape)" },
    { name: 'data/input.csv', content: 'a,b\n1,2\n3,4\n' },
    // a binary file via Buffer → SDK sets encoding:"base64"
    { name: 'fixtures/sheet.xlsx', data: readFileSync('./sheet.xlsx') },
  ],
});

By default, binary files larger than inlineThresholdBytes (DEFAULT_INLINE_THRESHOLD_BYTES, 256 KiB) are routed to the content-addressed blob store automatically; smaller ones are inlined. Tune the threshold in the constructor:

const client = new CodeRunnerClient({ baseUrl, token, inlineThresholdBytes: 512 * 1024 });

Large & shared files (blobs)

For files that are large or reused across many runs, upload them once by sha256 and reference the returned ref from any job — re-running the same upload skips the PUT entirely:

// upload returns a content address you can reuse
const { ref } = await client.blobs.upload(readFileSync('./train.parquet'));

await client.executeFiles({
  language: 'python',
  files: [
    { name: 'main.py', content: "import pandas as pd; print(pd.read_parquet('data/train.parquet').shape)" },
    { name: 'data/train.parquet', ref }, // reference the blob by hash
  ],
});

Blob methods require the object store to be configured server-side; otherwise /v1/blobs/* returns 501. The helpers toFileInput/toFileInputs and the types SdkFileInput / TextFileInput / BinaryFileInput are also exported. See the Blobs guide.

Error handling

Every method throws typed errors that extend CodeRunnerError, so you can branch on instanceof:

import {
  CapacityError,
  ValidationError,
  RateLimitError,
  NotFoundError,
} from '@teovilla/code-runner-sdk-node';

try {
  await client.execute(req);
} catch (err) {
  if (err instanceof CapacityError) {
    // 429 — queue is full. err.retryAfterMs tells you when to retry.
  } else if (err instanceof ValidationError) {
    // 400 — bad body, unknown language/version.
  } else if (err instanceof RateLimitError) {
    // 429 on stdin — err.retryAfterMs / err.capBytes
  } else if (err instanceof NotFoundError) {
    // 404 — unknown job id.
  }
}

All errors carry .status (the HTTP code) and .body (the parsed response).

Sign channel auth

Authorizing a browser's private-run-<jobId> channel is your backend's job. The SDK gives you a helper so you never hand-roll the HMAC:

import { createChannelAuthorizer } from '@teovilla/code-runner-sdk-node';

const authorize = createChannelAuthorizer({
  appKey: process.env.SOKETI_APP_KEY!,
  appSecret: process.env.SOKETI_APP_SECRET!, // stays server-side
});

// In your channel-auth route (what pusher-js POSTs to):
app.post('/code-runner/auth', (req, res) => {
  const { socket_id, channel_name } = req.body;
  res.json(authorize(socket_id, channel_name)); // { auth: "<key>:<hmac>" }
});

The authorizer only signs private-run-* channels and throws otherwise. See Channel Auth for the full pattern and trust rationale.

OpenTelemetry (optional)

If you have @opentelemetry/api installed (an optional peer dependency), the SDK injects a W3C traceparent header into POST /v1/execute. The worker extracts it, so one execution produces a single connected trace spanning your backend, the API, and the worker. See Observability.

Next

On this page