voxal docs
SDK · Core

The connection

The conn object passed to every handler, covering id, terminal size, write, close, and key encoding

Every handler receives a conn: a single connected terminal session. It is how you read the terminal's size, write to the screen, and end the session. All of your app's users are multiplexed into one app instance, so each user gets their own conn.

interface Conn {
  readonly id: string;
  readonly cols: number;
  readonly rows: number;
  write(data: string): void;
  close(): void;
}

conn.id

readonly id: string

A unique identifier for this session. Use it as the key for per-connection state in a Map.

sessions.set(conn.id, { score: 0 });

conn.cols and conn.rows

readonly cols: number  // terminal width  (default 80)
readonly rows: number  // terminal height (default 24)

The current terminal size in character cells. If the size is unknown they default to 80 by 24. They are updated automatically before onResize fires, so reading them in any handler always gives the live size.

const rule = '-'.repeat(conn.cols);

conn.write(data)

write(data: string): void

Writes a string straight to the terminal. There is no buffering and no implicit newline. Non-string values are coerced with String().

conn.write('\x1b[2J\x1b[H');               // clear screen, cursor home
conn.write('\x1b[1;32mready\x1b[0m\r\n');  // bold green "ready", then a newline
conn.write('size: ' + conn.cols + 'x' + conn.rows + '\r\n');

Use \r\n, not \n

write sends bytes verbatim, so you control every character including line endings. Use "\r\n" to start a new line, not "\n". A bare \n moves the cursor down but does not return it to column 0, so successive lines stair-step to the right. The \r (carriage return) is what returns the cursor to the start of the line.

Forgetting the \r is the most common terminal-output bug. If your output drifts diagonally across the screen, you are missing carriage returns.

conn.close()

close(): void

Ends the SSH session. Your onClose handler still fires afterwards, so closing is a good place to request teardown and let onClose do the cleanup.

.onKey((conn, data) => {
  if (data === 'q' || data === '\x03') return conn.close();
})

Key encoding

The data string passed to onKey is the decoded input. It is usually a single character, but special keys arrive as multi-character escape sequences and control keys as control codes.

Inputdata
An ordinary keythe character, for example "q"
Enter"\r"
Ctrl-C"\x03"
Ctrl-D"\x04"
Up arrow"\x1b[A"

Escape sequences arrive whole, so you can match them with a single comparison. For richer decoding (named keys, modifiers), use the UI layer's useInput.

Terminal output notes

The sandbox is pure JavaScript with no DOM and no global fetch. A few things to keep in mind when writing to the terminal:

  • Include ANSI escapes yourself for color and cursor control. For example, "\x1b[2J\x1b[H" clears the screen and homes the cursor.
  • console.log goes to the server logs, not the user's terminal. Use conn.write for anything the user should see, and console.log for your own debugging.
  • There is no built-in network. Use the SDK fetch for HTTP requests; the global fetch does not exist in the sandbox.

If you would rather build with components than hand-write escape sequences, use the React UI layer.

A handy ANSI cheat sheet

SequenceEffect
\x1b[2JClear the entire screen
\x1b[HMove cursor to top-left (home)
\x1b[<row>;<col>HMove cursor to a position (1-based)
\x1b[KClear from the cursor to end of line
\x1b[?25l / \x1b[?25hHide / show the cursor
\x1b[1m ... \x1b[0mBold ... reset all attributes
\x1b[3<n>mForeground color (30 to 37)
\x1b[38;2;<r>;<g>;<b>m24-bit truecolor foreground

On this page