GitHub - sgbalogh/s2.term: Demo multi-player pseudo-terminal over S2 streams

3 min read Original article ↗

s2.term

Demo of a durable, log-structured pseudo-terminal implemented over S2 streams.

(See also the blog post about this.)

demo

About

This repo is meant to serve as a proof-of-concept. Please use at your own risk!

It's kinda like a remote shell, e.g. SSH, but:

  • Multiplayer, including from browsers. Useful for pair-programming.
  • All terminal updates are durable on object-storage before delivery.
  • All updates can be replayed (example); old state can be periodically trimmed.
  • No servers necessary, all I/O happens by reading or appending to the S2 API.
  • Configurable with ACLs.
    • For example, can configure roles to grant read-only access to terminal viewers, and read-write to others.

Organization

There are two components in this repo currently:

  • An xterm.js based frontend (ts-frontend), which can be run locally, or deployed on any web server.
  • A Rust binary that acts as PTY (rust-pty-host), which will run on the remote host where you want a shell.

Running it

S2 Setup

First, head to S2.dev and create an account and token if you haven't already.

Important

If you plan on sharing access to your terminal with others, make sure to create dedicated (and limited) tokens with appropriate scopes. These can be revoked at anytime via the S2 dashboard.

Organization

Each terminal "session" requires two S2 streams. This setup assumes an organization like the following:

s2://${MY_BASIN}/sessions/${MY_SESSION}/term_input
s2://${MY_BASIN}/sessions/${MY_SESSION}/term_output

For example, my basin looks like this:

% s2 ls s2term-00001
s2://s2term-00001/sessions/0001/term_input 2025-06-06T22:03:22Z
s2://s2term-00001/sessions/0001/term_output 2025-06-06T22:03:26Z
s2://s2term-00001/sessions/0002/term_input 2025-06-06T22:33:26Z
s2://s2term-00001/sessions/0002/term_output 2025-06-06T22:33:31Z
s2://s2term-00001/sessions/0003/term_input 2025-06-06T22:51:30Z
s2://s2term-00001/sessions/0003/term_output 2025-06-06T22:51:34Z
s2://s2term-00001/sessions/0004/term_input 2025-06-09T21:37:57Z
s2://s2term-00001/sessions/0004/term_output 2025-06-09T21:39:00Z

Create necessary streams

This can be done using the S2 CLI.

export MY_ACCESS_TOKEN="<access token>"
export MY_BASIN="s2term-00001"
export MY_SESSION="0001"

# input stream
s2 create-stream \
  s2://${MY_BASIN}/sessions/${MY_SESSION}/term_input \
  --storage-class express \
  --timestamping-mode client-require

# output stream
s2 create-stream \
  s2://${MY_BASIN}/sessions/${MY_SESSION}/term_output \
  --storage-class express \
  --timestamping-mode client-require

Starting the Rust PTY

Make sure you have Rust installed.

Start the process with cargo. In the example below, we will manage a PTY (over a zsh shell, though you can use any process you like) for a single session.

export S2_ACCESS_TOKEN="<access token>"
export RUST_LOG=info
cargo run --release -- \
  "${MY_BASIN}" \
  "${MY_SESSION}" \
    --process zsh 

You can toggle trace-level logs with RUST_LOG=info,s2term_pty_host=trace.

Running the PTY via Docker

Alternatively, run the PTY via a Docker container for some additional security.

docker build -t s2term-pty-host .
docker run \
  -e "S2_ACCESS_TOKEN=${S2_ACCESS_TOKEN}" \
  s2term-pty-host \
    "${MY_BASIN}" \
    "${MY_SESSION}" \
      --process zsh 

Starting the frontend

Install Node.js.

Then you can install the frontend with:

cd ./ts-frontend
npm install

... and run a local dev server with

You should be able to see the site running at http://localhost:3000.

By default, the xterm div will start by interactively prompting for basin, session, and token. These can be provided also via query params, e.g.:

http://localhost:3000/?basin=s2term-00001&session=0001&token=MY_TOKEN

Similarly, some other query params:

  • since=EPOCH_MS
    • If an epoch timestamp in ms is provided, the session will attempt to replay from messages starting at that moment.
  • speedup=MULTIPLE
    • In combination with a since timestamp, this factor controls how long to wait between records when replaying historical messages. E.g. speedup=60 would advance time at a rate of 1 min per second.