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-nodenpm install @teovilla/code-runner-sdk-nodeyarn add @teovilla/code-runner-sdk-nodeRequires 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
});| Option | Type | Notes |
|---|---|---|
baseUrl | string | The gateway base URL. |
token | string | EXECUTOR_API_TOKEN; sent as Authorization: Bearer …. |
fetch | FetchLike? | Optional custom fetch; defaults to globalThis.fetch. |
Client methods
| Method | Endpoint | Description |
|---|---|---|
listLanguages() | GET /v1/languages | List available languages & versions. |
execute(req) | POST /v1/execute | Enqueue a job (raw passthrough) → { jobId, channel, status }. |
executeFiles({ language, version?, files }) | POST /v1/execute | Convenience submit: accepts text files, binary Buffers, and raw FileInputs, routing each inline-vs-CAS automatically. |
blobs.upload(buffer, { ttlSeconds? }) | blob handshake | Upload a buffer to the blob store → { ref } (skips the PUT if already present). |
blobs.check(hashes) | POST /v1/blobs/check | Low-level existence check passthrough. |
getJob(id) | GET /v1/jobs/:id | Poll job status. |
getOutput(id) | GET /v1/jobs/:id/output | Pull 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/start | Send the start signal (the handshake). |
sendStdin(id, chunk) | POST /v1/jobs/:id/stdin | Write a chunk to the process stdin. |
closeStdin(id) | POST /v1/jobs/:id/stdin/close | Signal EOF (Ctrl-D). |
kill(id) | POST /v1/jobs/:id/kill | SIGKILL 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.