code-runner
Guides

Interactive Sessions

The full backend + frontend flow for a live interactive stdin round-trip.

This guide ties the backend SDK and the React SDK into one working interactive session. The example runs a Python program that prompts for input, and lets a browser type into it live.

Who does what

There are three actors. Keeping their roles straight is the whole game:

ActorHoldsResponsibilities
Browser (React)nothing secretSubscribes to soketi output; relays user input to your backend.
Your backend (Node SDK)EXECUTOR_API_TOKEN, SOKETI_APP_SECRETCalls the code-runner API; signs channel auth.
code-runnerruns the sandboxExecutes, streams output, accepts stdin/control.

The browser never talks to code-runner directly — only to your backend, which forwards to the bearer-authed API.

Backend routes

Expose a few thin routes that wrap the SDK. (Express shown; any framework works.)

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

const client = new CodeRunnerClient({
  baseUrl: process.env.CODE_RUNNER_URL!,
  token: process.env.EXECUTOR_API_TOKEN!,
});
const authorize = createChannelAuthorizer({
  appKey: process.env.SOKETI_APP_KEY!,
  appSecret: process.env.SOKETI_APP_SECRET!,
});

const app = express();
app.use(express.json());

// 1. Enqueue a job (do NOT start it yet)
app.post('/api/jobs', async (req, res) => {
  const job = await client.execute({
    language: 'python',
    files: [{ name: 'main.py', content: "name = input('name? ')\nprint(f'hello {name}')\n" }],
  });
  res.json(job); // { jobId, channel, status }
});

// 2. Channel auth — pusher-js POSTs here from the browser
app.post('/code-runner/auth', (req, res) => {
  const { socket_id, channel_name } = req.body;
  res.json(authorize(socket_id, channel_name));
});

// 3. Start the job (after the browser has subscribed)
app.post('/api/jobs/:id/start', (req, res) =>
  client.start(req.params.id).then(() => res.json({ ok: true })),
);

// 4. Forward stdin / kill
app.post('/api/jobs/:id/stdin', (req, res) =>
  client.sendStdin(req.params.id, req.body.chunk).then(() => res.json({ ok: true })),
);
app.post('/api/jobs/:id/kill', (req, res) =>
  client.kill(req.params.id).then(() => res.json({ ok: true })),
);

app.listen(3000);

Frontend

import { useState } from 'react';
import {
  CodeRunnerProvider,
  useCodeRunnerJob,
} from '@teovilla/code-runner-react';

export function App() {
  return (
    <CodeRunnerProvider
      appKey={import.meta.env.VITE_SOKETI_APP_KEY}
      host="localhost"
      port={6001}
      authEndpoint="/code-runner/auth"
    >
      <Runner />
    </CodeRunnerProvider>
  );
}

function Runner() {
  const [jobId, setJobId] = useState<string | null>(null);

  async function run() {
    // 1. Ask our backend to enqueue
    const job = await fetch('/api/jobs', { method: 'POST' }).then((r) => r.json());
    setJobId(job.jobId);
    // The hook below subscribes to the channel. Once subscribed, we start it.
    await fetch(`/api/jobs/${job.jobId}/start`, { method: 'POST' });
  }

  return jobId ? <Job jobId={jobId} /> : <button onClick={run}>Run</button>;
}

function Job({ jobId }: { jobId: string }) {
  const { stage, stdout, result, sendStdin } = useCodeRunnerJob({
    jobId,
    onStdin: (chunk) =>
      fetch(`/api/jobs/${jobId}/stdin`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ chunk }),
      }),
  });

  return (
    <div>
      <p>stage: {stage}</p>
      <pre>{stdout}</pre>
      <button onClick={() => sendStdin('World\n')}>Answer "World"</button>
      {result && <p>exit {result.exitCode}</p>}
    </div>
  );
}

Subscribe before you start

In the snippet above, run() calls /start immediately after /api/jobs. In a robust implementation you should wait until the hook reports the channel is subscribed before starting, so the very first output isn't missed. The simplest pattern: render <Job> (which subscribes on mount), and trigger /start from an effect once the subscription is confirmed. See the start-handshake.

What the round-trip looks like

browser  → POST /api/jobs                  → backend → POST /v1/execute → {jobId, channel}
browser  ← subscribe private-run-<id>      (auth signed by your backend)
browser  → POST /api/jobs/:id/start        → backend → POST /v1/jobs/:id/start
worker   → python main.py
worker   → stdout "name? "                 → soketi → browser renders prompt
browser  → POST /api/jobs/:id/stdin "World\n" → backend → POST /v1/jobs/:id/stdin
worker   → stdin pipe → process
worker   → stdout "hello World\n"          → soketi → browser
worker   → result { exitCode: 0 }          → soketi → browser

Without a browser

If you only need a programmatic round-trip (no browser), your backend can both submit and subscribe — connect pusher-js (or any Pusher client) server-side and sign the channel locally. The bundled apps/stub does exactly this and is the basis of make e2e. The mechanics are identical; the browser is just one possible subscriber.

On this page