OpenREPL initially Forked from gotty. GoTTY is a simple command line tool that turns your CLI tools into web applications.
This Fork is intended to create web based REPL services for various programming languages using gotty and containers. You can checkout on our website for more info on REPL playgrounds and try them as well: openrepl.com (earlier gorepl.com)
Run it locally: see Run locally on macOS (Colima).
Design docs: see Architecture (High-Level Design) below and the low-level designs in
docs/.
Fork openrepl to start the REPL servers in your local system, Please make sure all pre-requisites are installed.
(Files named with darwin_amd64 are for Mac OS X users)
You can install GoTTY REPL server as shown below:
$ cd src
$ make all
$ ../bin/gotty -w$ cd src
$ make buildgo GOROOT_BOOTSTRAP=/usr/lib/go-x.xx/ (non x86_64 machines)
$ make deb
$ sudo dpkg -i ../deb/gotty.deb- GoTTY requires go1.9 or later.
- npm
- webpack
- cling
- gointerpreter
- python2.7
- xterm
- all requisites can be installed using ./install_prerequisite.sh
To deploy and run a local copy of openrepl :
-
Pull the Docker image
docker pull vickeykumar/openrepl:latest
-
Run the image
-
without nested containers.
docker run -itd --name openrepl \ -p 8080:8080 vickeykumar/openrepl:latest \ -p 8080
-
with nested containers (containerized REPLs inside docker, nested containers)
docker run -itd --name openrepl \ -v /sys/fs/cgroup:/sys/fs/cgroup:rw --privileged \ -p 8080:8080 vickeykumar/openrepl:latest \ -p 8080
-
-
open localhost:8080 in your browser to check repls offline
```bash
docker run -itd --name openrepl \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw --privileged \
-p 8080:8080 vickeykumar/openrepl:latest \
-p 8080 [ <other CLI Options>]
```
The OpenREPL image is amd64 only: the bundled Go toolchain, go-bindata, cling, evcxr and gdb are x86-64 binaries. On an Apple Silicon Mac, run it in a Colima VM that uses Rosetta to emulate x86-64.
Paste the commands without comments. zsh does not treat
#as a comment when you paste commands.
brew install colima docker docker-buildx
mkdir -p ~/.docker/cli-plugins
ln -sfn "$(brew --prefix)/opt/docker-buildx/bin/docker-buildx" ~/.docker/cli-plugins/docker-buildx
softwareupdate --install-rosetta --agree-to-license
colima start openrepl --vm-type vz --vz-rosetta --cpu 4 --memory 8 --disk 80 --kubernetes=false
docker context use colima-openrepl
docker run --rm --platform linux/amd64 ubuntu:22.04 uname -mThe last command should print x86_64. The build needs at least 8 GB of VM memory and 80 GB of disk.
On an Intel Mac, leave out --vm-type vz --vz-rosetta, and leave out every --platform linux/amd64 in this section.
Use this the first time, after changing the Dockerfile or install_prerequisite.sh, or to test the exact image you deploy. The first build is slow because it installs every language toolchain. Later builds reuse that layer.
cd ~/Documents/openrepl/openrepl
docker build --platform linux/amd64 -t openrepl:dev .
docker run -d --name openrepl-dev --platform linux/amd64 -p 8080:80 openrepl:dev
open http://localhost:8080To view the server logs, run docker exec openrepl-dev tail -f /gottyTraces/gotty.log.
To pick up changes, rebuild the image and replace the container:
docker build --platform linux/amd64 -t openrepl:dev .
docker rm -f openrepl-dev
docker run -d --name openrepl-dev --platform linux/amd64 -p 8080:80 openrepl:devFor day-to-day changes to HTML, CSS, JS, TypeScript, demos.xml or Go code, mount the repo into the build stage instead of rebuilding the whole image. Stop openrepl-dev first (docker rm -f openrepl-dev), because both use port 8080.
Start the dev container once, in its own terminal tab:
cd ~/Documents/openrepl/openrepl
docker build --platform linux/amd64 --target build-image -t openrepl:build .
docker run -it --rm --name openrepl-devbox --platform linux/amd64 -p 8080:8080 -v "$PWD":/opt/openrepl -w /opt/openrepl/src openrepl:build bashInside the container, build everything once:
make allThe build finds the repo root on its own, even though git inside the container refuses the mounted folder (it belongs to your Mac user, and the container runs as root). To use git commands inside the container, run git config --global --add safe.directory /opt/openrepl each time you start it. The container is started with --rm, so the setting doesn't survive a restart.
After each change, press Ctrl+C in the container, run the line below, then hard-refresh the browser (Cmd+Shift+R):
rm -rf bindata && make gotty && ../bin/gotty -w -p 8080 --title-format '<fmt><title>{{ .command }}</title><jid>{{ encodePID .pid }}</jid></fmt>'rm -rf bindata forces every web asset to be re-embedded. Without it, the Makefile misses edits inside existing files under src/resources/js/.
Run the Go unit tests inside the dev container:
GO111MODULE=off GOPATH=/opt/openrepl ../go_1.19/go/bin/go test -race gateway tunnel trustedThen do a quick manual check at localhost:8080:
- Open a Python REPL.
- Run code from the editor, and debug a C file.
- In the file browser, create, upload and download a file.
- Add a terminal tab and fork a REPL.
- Open the share link in a second browser.
- Sign in.
- No per-REPL sandboxing locally. Colima's VM uses cgroup v2, so the log shows
Unable to create Containerand REPLs run without their own namespaces or memory limits. Test sandboxing on a cgroup v1 host. - Debug runs under QEMU locally. Rosetta cannot trace programs, so plain gdb fails with
Couldn't get registers. Debug detects it and runs your program under QEMU, which gdb connects to (see LLD 04). The program starts paused: set breakpoints, then typec. The assembly REPL (rappel) needs real ptrace and does not start here. - Don't commit
bin/gotty. The dev container rebuilds this tracked file. Restore it before committing withgit checkout -- bin/gotty, and don't commitnode_modulesordistfolders. - Genie and Practice question generation need an OpenAI key. Set
OPENREPL_OPENAI_API_KEY(see Settings and secrets), then restart the server. - Stopping and restarting the VM:
colima stop openreplandcolima start openrepl.colima delete openreplremoves the VM and its images.
One public server, the gateway, serves the site and hands REPL sessions to workers. A worker needs no public port: it connects out to the gateway. Same binary, same URLs for the browser.
The picture below is a small fleet: one gateway and two workers.
flowchart TB
B["Browser<br/>openrepl.example.com"]
subgraph GW["Gateway (the only public server)"]
direction TB
SITE["Site<br/>pages, sign-in, blog, admin,<br/>Genie proxy"]
RT["Router<br/>picks a node by weight,<br/>keeps a visitor on it"]
TS["Tunnel server<br/>/api/tunnel"]
LR["Own REPLs<br/>(weight 10, or 0 to only route)"]
GD[("Databases and settings<br/>files, MongoDB or Firestore")]
GH[("Copy of every home<br/>with --workspace-sync")]
end
subgraph W1["worker-01 (no public port)"]
direction TB
T1["Tunnel client"]
R1["REPL sandboxes<br/>namespaces + cgroup"]
H1[("/tmp/home<br/>users' files")]
end
subgraph W2["worker-02 (no public port)"]
direction TB
T2["Tunnel client"]
R2["REPL sandboxes<br/>namespaces + cgroup"]
H2[("/tmp/home<br/>users' files")]
end
EXT["Firebase, OpenAI, OpenRouter"]
B -- "HTTPS and WSS" --> SITE
SITE --> RT
SITE --> GD
SITE -- "API keys stay here" --> EXT
RT --> LR
RT --> TS
T1 -- "connects out: wss or ssh,<br/>shared token" --> TS
T2 -- "connects out: wss or ssh,<br/>shared token" --> TS
T1 --> R1 --> H1
T2 --> R2 --> H2
H1 <-. "workspace sync" .-> GH
H2 <-. "workspace sync" .-> GH
- The browser talks to the gateway only. Pages, sign-in, Genie and the admin dashboard are served there. A terminal or a file request is passed through the tunnel to the node that holds the visitor's session.
- Workers connect out to
/api/tunnelwith the shared token, so they can sit behind NAT or a home router. Each tells the gateway its name, weight and languages; the gateway sends back the site's rules (maintenance, switched-off languages). - The router places a new visitor on a node at random, in proportion to the weights, and keeps them there. With two workers of weight 10 and a gateway of weight 10, each takes about a third.
- Data: accounts, settings and the other databases live on the gateway (see Data and storage). Users' files live on the node that runs their session, and on the gateway too with
--workspace-sync. - If a worker stops, its visitors see a countdown; with
--workspace-synctheir session moves to another node with its files.
1. Create a shared token
openssl rand -hex 322. Start the gateway (the public server)
GOTTY_WORKER_TOKEN=<token> gotty -w --mode=gateway --port 803. Start each worker
GOTTY_WORKER_TOKEN=<token> gotty -w --mode=worker --worker-server wss://gateway.example.com/api/tunnelNew visitors are now spread over the gateway and the workers at random, in proportion to each node's weight, and each visitor stays on one node. Check the fleet in the admin dashboard, under Workers: it also shows how many sessions the random choice has given each node, next to the share its weight should give it.
4. Keep a copy of the users' files on the gateway (optional). Restart the gateway with --workspace-sync. Workers reconnect by themselves and need no option, only the same version of the binary.
GOTTY_WORKER_TOKEN=<token> gotty -w --mode=gateway --port 80 --workspace-syncEach user's files are now kept in step, both ways, between the worker that runs them and the gateway. If a worker stops, the file browser, downloads, saves and uploads keep working from the gateway's copy. From the moment the worker drops, the page shows a countdown and keeps Reconnect and Run disabled until it ends. After 2 minutes without the worker (--relocate-after changes this), the session continues on the gateway or another worker with all its files; a program that was running is lost. A home is copied the first time its session is used after the option is on, so start it before you need it. Without the option the gateway holds no copy, and a stopped worker means "execution node unavailable". Details in the operator guide.
A worker opens no port. Add --port 9090 if people on the same network should also be able to open it directly (see the operator guide).
Useful options:
| Option | Where | Effect |
|---|---|---|
--local-weight 0 |
gateway | The gateway only routes; all sessions run on workers. |
--workspace-sync |
gateway | Keep a copy of every home on the gateway (step 4). |
--worker-weight 30 |
worker | Three times the share of a worker with the default 10. |
--worker-id worker-01 |
worker | A name for the worker; defaults to its host name. |
1. Keep the token in a file, readable only by the service user, with the same content on the gateway and every worker. Use a new token, not one from a test run.
umask 077
echo "GOTTY_WORKER_TOKEN=$(openssl rand -hex 32)" > /etc/openrepl/tunnel.env2. Run the gateway with HTTPS, either in the gateway itself or in a proxy (nginx, a load balancer) in front of it.
set -a; . /etc/openrepl/tunnel.env; set +a
gotty -w --mode=gateway --port 443 --max-connection 2564 \
--tls --tls-crt /etc/openrepl/fullchain.pem --tls-key /etc/openrepl/privkey.pem3. Connect workers with wss:// and the gateway's public name. Never use ws:// outside a test: it sends the token in clear text.
set -a; . /etc/openrepl/tunnel.env; set +a
gotty -w --mode=worker --worker-id worker-01 --worker-server wss://openrepl.example.com/api/tunnel4. No HTTPS between worker and gateway? Use SSH instead of ws://. It is encrypted, and the worker checks the gateway's key before it sends the token. Start the gateway with --tunnel-addr 0.0.0.0:2222 (open that port to the workers only), and the worker with:
set -a; . /etc/openrepl/tunnel.env; set +a
gotty -w --mode=worker --worker-id worker-01 \
--worker-server ssh://10.0.0.5:2222 --worker-hostkey 'SHA256:<fingerprint>'The fingerprint is in the gateway's /gottyTraces/gotty.log (tunnel host key SHA256:...), or run ssh-keygen -lf ~/.gotty.tunnel_key on the gateway. The gateway creates that key on first start; keep the file, or the fingerprint changes.
5. Run both as services that restart on failure (systemd EnvironmentFile=/etc/openrepl/tunnel.env, or docker run --env-file). A worker reconnects by itself when the gateway restarts.
6. Before stopping a worker, drain it (Workers, then Drain) and wait for its terminals to finish. From a script:
curl -b "user-session=<admin session cookie>" -H 'X-Requested-With: openrepl-admin' -X POST https://openrepl.example.com/admin/workers/worker-01/drainEvery worker needs the same REPLs and sandbox setup as a normal OpenREPL server (the same image). Users' files live on the node that runs their sessions, and with --workspace-sync on the gateway too. Give /tmp/home durable storage on the nodes whose loss you cannot accept, and with --workspace-sync give /opt/gotty/wsync (--sync-state-dir) durable storage on the gateway and every worker: it holds the records that tell a deleted file from a new one.
With two containers, use ws:// (the test gateway has no HTTPS) and the gateway container's address and port as the worker sees them, not localhost, which is the worker itself:
GOTTY_WORKER_TOKEN=<token> gotty -w --mode=worker --worker-server ws://<gateway container IP>:<gateway port>/api/tunnelEverything else (config files, private certificates, the nginx, Docker and systemd examples, every option, troubleshooting) is in the operator guide.
Sign in with an account listed in OPENREPL_ADMIN_EMAILS and open /admin (an Admin link appears in the account menu). One page shows how the site is doing and lets you run it:
- Overview, Health, Parameters, Log, Audit log: counts, charts of terminals per day and language, health checks, how the server was started (secrets are never shown), the end of the log with credentials masked, and every change an admin made.
- Workers and Sessions (gateway mode): drain, undrain or reconnect a worker, see who is on which node, end a session or move it to another node, and the command line for adding a worker.
- Site settings: colour of the day, an announcement banner, maintenance mode, switching off a broken language, and the Genie switch and its rate limits.
- Feedback, Shared code, Users: read and clear feedback (export as CSV), remove a shared snippet, sign a user out everywhere or block them.
The design and the API behind it are in LLD 13.
Genie in the chat panel, and Ask Genie in the blog editor, can answer from the site's own notes so that they do not invent features. The notes are short Markdown files in src/resources/knowledge/, one topic each, compiled into the binary; the blog posts are searched too, from the blog store. A note starts with a header:
---
title: Sharing a live session
keywords: share, send, link, friend, teammate
link: /about.html
---
The text, 80 to 220 words, public.
The server picks the notes that match a question by words (no extra service or cost) and adds them to the request, and the chat panel shows which ones it used. To fix a missed question, add the word people used to the note's keywords, and rebuild. Keep to text that could be on a public page: notes are sent to the model. Details in LLD 07, section 3a.
Usage: gotty [options] <command> [<arguments...>]
Run gotty with your preferred command as its arguments (e.g. gotty top).
By default, GoTTY starts a web server at port 8080. Open the URL on your web browser and you can see the running command as if it were running on your terminal.
--address value, -a value IP address to listen (default: "0.0.0.0") [$GOTTY_ADDRESS]
--port value, -p value Port number to liten (default: "8080") [$GOTTY_PORT]
--permit-write, -w Permit clients to write to the TTY (BE CAREFUL) [$GOTTY_PERMIT_WRITE]
--credential value, -c value Credential for Basic Authentication (ex: user:pass, default disabled) [$GOTTY_CREDENTIAL]
--random-url, -r Add a random string to the URL [$GOTTY_RANDOM_URL]
--random-url-length value Random URL length (default: 8) [$GOTTY_RANDOM_URL_LENGTH]
--tls, -t Enable TLS/SSL [$GOTTY_TLS]
--tls-crt value TLS/SSL certificate file path (default: "~/.gotty.crt") [$GOTTY_TLS_CRT]
--tls-key value TLS/SSL key file path (default: "~/.gotty.key") [$GOTTY_TLS_KEY]
--tls-ca-crt value TLS/SSL CA certificate file for client certifications (default: "~/.gotty.ca.crt") [$GOTTY_TLS_CA_CRT]
--index value Custom index.html file [$GOTTY_INDEX]
--title-format value Title format of browser window (default: "<fmt><title>{{ .command }}</title><jid>{{ encodePID .pid }}</jid></fmt>") [$GOTTY_TITLE_FORMAT]
--reconnect Enable reconnection [$GOTTY_RECONNECT]
--reconnect-time value Time to reconnect (default: 10) [$GOTTY_RECONNECT_TIME]
--max-connection value Maximum connection to gotty (default: 0) [$GOTTY_MAX_CONNECTION]
--once Accept only one client and exit on disconnection [$GOTTY_ONCE]
--timeout value Timeout seconds for waiting a client(0 to disable) (default: 0) [$GOTTY_TIMEOUT]
--permit-arguments Permit clients to send command line arguments in URL (e.g. http://example.com:8080/?arg=AAA&arg=BBB) [$GOTTY_PERMIT_ARGUMENTS]
--width value Static width of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_WIDTH]
--height value Static height of the screen, 0(default) means dynamically resize (default: 0) [$GOTTY_HEIGHT]
--ws-origin value A regular expression that matches origin URLs to be accepted by WebSocket. No cross origin requests are acceptable by default [$GOTTY_WS_ORIGIN]
--term value Terminal name to use on the browser, one of xterm or hterm. (default: "xterm") [$GOTTY_TERM]
--mode value Run mode: standalone, gateway or worker (default: "standalone") [$GOTTY_MODE]
--worker-token value Shared secret between the gateway and its workers (prefer the GOTTY_WORKER_TOKEN environment variable) [$GOTTY_WORKER_TOKEN]
--local-weight value Gateway: its own share of new sessions next to the workers, 0 makes it routing-only (default: 10) [$GOTTY_LOCAL_WEIGHT]
--tunnel-path value Gateway: path of the WebSocket endpoint workers connect to (default: "/api/tunnel") [$GOTTY_TUNNEL_PATH]
--tunnel-addr value Gateway: also accept workers over raw SSH on this address (e.g. 0.0.0.0:2222), disabled when empty [$GOTTY_TUNNEL_ADDR]
--tunnel-hostkey value Gateway: SSH host key file for the worker tunnel, created if missing (default: "~/.gotty.tunnel_key") [$GOTTY_TUNNEL_HOSTKEY]
--worker-server value Worker: gateway URL, wss://host/api/tunnel or ssh://host:port [$GOTTY_WORKER_SERVER]
--worker-hostkey value Worker: SHA256 fingerprint of the gateway tunnel host key (required for ssh://) [$GOTTY_WORKER_HOSTKEY]
--worker-id value Worker: unique id, defaults to the hostname [$GOTTY_WORKER_ID]
--worker-weight value Worker: relative share of new sessions (default: 10) [$GOTTY_WORKER_WEIGHT]
--worker-languages value Worker: comma separated REPL commands it can run (e.g. python,bash,cling), empty means all [$GOTTY_WORKER_LANGUAGES]
--workspace-sync Gateway: keep a copy of every worker's homes on the gateway, in step with the worker [$GOTTY_WORKSPACE_SYNC]
--sync-state-dir value Gateway and worker: where workspace sync keeps its records, keep it on durable storage (default: "/opt/gotty/wsync") [$GOTTY_SYNC_STATE_DIR]
--relocate-after value Gateway with --workspace-sync: how long a worker may be away before its sessions are placed elsewhere, e.g. 30s or 2m (default: "2m") [$GOTTY_RELOCATE_AFTER]
--worker-capacity value Worker: memory budget in MB for sessions, 0 derives it from RAM (default: 0) [$GOTTY_WORKER_CAPACITY]
--close-signal value Signal sent to the command process when gotty close it (default: SIGHUP) (default: 1) [$GOTTY_CLOSE_SIGNAL]
--close-timeout value Time in seconds to force kill process after client is disconnected (default: -1) (default: -1) [$GOTTY_CLOSE_TIMEOUT]
--config value Config file path (default: "~/.gotty") [$GOTTY_CONFIG]
--env-file value File of OPENREPL_* and GOTTY_* settings, loaded before anything else (a default that does not exist is ignored, one you name must) (default: "~/.env") [$GOTTY_ENV_FILE]
--version, -v print the version
You can customize default options and your terminal (hterm) by providing a config file to the gotty command. GoTTY loads a profile file at ~/.gotty by default when it exists.
// Listen at port 9000 by default
port = "9000"
// Enable TSL/SSL by default
enable_tls = true
// hterm preferences
// Smaller font and a little bit bluer background color
preferences {
font_size = 5
background_color = "rgb(16, 16, 32)"
}
See the .gotty file in this repository for the list of configuration options.
By default, GoTTY doesn't allow clients to send any keystrokes or commands except terminal window resizing. When you want to permit clients to write input to the TTY, add the -w option. However, accepting input from remote clients is dangerous for most commands. When you need interaction with the TTY for some reasons, consider starting GoTTY with tmux or GNU Screen and run your command on it (see "Sharing with Multiple Clients" section for detail).
To restrict client access, you can use the -c option to enable the basic authentication. With this option, clients need to input the specified username and password to connect to the GoTTY server. Note that the credentical will be transmitted between the server and clients in plain text. For more strict authentication, consider the SSL/TLS client certificate authentication described below.
The -r option is a little bit casualer way to restrict access. With this option, GoTTY generates a random URL so that only people who know the URL can get access to the server.
All traffic between the server and clients are NOT encrypted by default. When you send secret information through GoTTY, we strongly recommend you use the -t option which enables TLS/SSL on the session. By default, GoTTY loads the crt and key files placed at ~/.gotty.crt and ~/.gotty.key. You can overwrite these file paths with the --tls-crt and --tls-key options. When you need to generate a self-signed certification file, you can use the openssl command.
openssl req -x509 -nodes -days 9999 -newkey rsa:2048 -keyout ~/.gotty.key -out ~/.gotty.crt(NOTE: For Safari uses, see how to enable self-signed certificates for WebSockets when use self-signed certificates)
For additional security, you can use the SSL/TLS client certificate authentication by providing a CA certificate file to the --tls-ca-crt option (this option requires the -t or --tls to be set). This option requires all clients to send valid client certificates that are signed by the specified certification authority.
To build the frontend part (JS files and other static files), you need npm.
This section gives the big picture. Each component has a low-level design (LLD) in docs/.
OpenREPL is one Go binary (bin/gotty, a fork of GoTTY). The same binary does three jobs:
- Serves the web app. The HTML, CSS, JS and images are compiled into the binary with
go-bindata. - Starts a REPL for each browser terminal. Each REPL runs as a child process on a pseudo-terminal (PTY), inside a lightweight container made of Linux namespaces and a cgroup v1 memory limit.
- Streams the terminal over a WebSocket. It relays PTY output to the browser (xterm.js) and keystrokes back to the REPL.
These services sit around the core:
- Firebase: Authentication (sign-in) and the Realtime Database (live sharing of a REPL, and Genie chat history).
- OpenAI and OpenRouter: reached only through a server-side proxy, for the Genie assistant and Practice question generation.
- A database, optional: the server keeps its own data (accounts and sessions, feedback, blog, shared code, practice progress, admin settings) in files on its disk by default, or in MongoDB or Firestore when one is configured. See Data and storage.
flowchart LR
subgraph Browser["Browser (openrepl.com)"]
UI["index.html + scribbler.js<br/>Ace editor, jstree file browser"]
TERM["gotty-bundle.js<br/>xterm.js terminal tabs"]
CHAT["chat-widget.js<br/>Genie assistant"]
end
subgraph Host["OpenREPL host (Docker or systemd)"]
subgraph GOTTY["gotty process"]
HTTP["HTTP mux<br/>pages, REST APIs"]
WS["WebSocket handlers<br/>/ws_<repl>"]
WT["webtty<br/>protocol bridge"]
LC["localcommand<br/>PTY + process"]
FB["filebrowser<br/>fsnotify watcher"]
CP["chat proxy<br/>/chat/completions"]
end
CG["containers<br/>namespaces + cgroup v1"]
REPL["REPL processes<br/>cling, python, node, ..."]
FS[("/tmp/home/*<br/>user workspaces")]
PS["persist<br/>one key-value interface"]
DB[("/opt/gotty/*.db<br/>UnQLite files (default)")]
end
RDB[("MongoDB or Firestore<br/>(optional, instead of the files)")]
FAUTH["Firebase Auth"]
FRTDB["Firebase Realtime DB"]
OAI["OpenAI / OpenRouter API"]
UI -- "HTTPS" --> HTTP
TERM -- "WSS (webtty protocol)" --> WS
WS --> WT --> LC --> CG --> REPL
LC -. "cwd / HOME" .-> FS
FB -. "watch" .-> FS
FB -- "file events" --> WT
HTTP --> PS
PS --> DB
PS -. "when configured" .-> RDB
CHAT -- "HTTPS" --> CP -- "API key added server-side" --> OAI
UI -- "sign-in" --> FAUTH
TERM -- "share / mirror" --> FRTDB
CHAT -- "chat history" --> FRTDB
| Component | Source | Responsibility |
|---|---|---|
| Entrypoint and config | src/gotty/main.go, src/utils/flags.go, .gotty |
Loads settings in this order: defaults, then the HCL config file, then CLI flags. Opens the databases, creates the containers, starts the server and handles graceful shutdown. |
| HTTP server | src/server/ |
Routing, middleware (logging, gzip, basic auth), pages, and the REST APIs: login, profile, feedback, blog, demo, file browser, upload, chat proxy. |
| WebTTY | src/webtty/ |
Transport-agnostic bridge between a master (the browser connection) and a slave (the PTY), using GoTTY's single-byte message protocol. |
| Local command backend | src/backend/localcommand/, src/github.com/kr/pty/ (patched) |
Builds the REPL command line and environment, starts it on a PTY, handles resize and close. |
| Containers | src/containers/ |
One parent cgroup per REPL type and one child cgroup per process with a memory limit. Also sets up the namespaces (UTS, PID, mount, net, user) and joins forked sessions through nsenter. |
| File browser | src/filebrowser/ |
Workspace tree, a 50 MB quota, and fsnotify events pushed to the browser. |
| Users and sessions | src/user/, src/cookie/ |
Firebase-backed login sessions and the signed session cookie. Maps each user to a home directory. Accounts can be blocked by an admin. |
| Persistence | src/persist/, src/cachedb/ |
One key-value interface (persist.Store) under every database of the site, with three backends: UnQLite files, MongoDB and Firestore. cachedb adds an in-memory read cache for the user database. See Data and storage. |
| Utilities | src/utils/, src/encoder/ |
Constants, the job scheduler (removes guest workspaces), demos.xml types, AES-GCM helpers, and the process-id encoding used for fork links. |
| REPL catalog | src/resources/meta/demos.xml |
One <Demo> per REPL: the demo animation, usage, docs link, starter code, and the <Compiler> script used by Run. |
| Web frontend | src/resources/, src/js/ |
Landing page and IDE (index.html, and scribbler.js built from js/src/page/), terminal engine (js/src/*.ts → gotty-bundle.js), Genie chat widget, Practice pages, and the JavaScript console (jsconsole). |
- Open a REPL. The user picks a language. The browser opens
wss://…/ws_<repl>and sends an init message:{Arguments, AuthToken, Payload}. The server resolves the user's home directory, starts the REPL on a PTY inside new namespaces and its own memory cgroup, and pipes I/O through WebTTY. Output is base64-encoded, and file-system changes arrive asEventmessages. - Run or debug editor code. Run reconnects the terminal with the editor content in the init payload (
IdeLang,IdeContent,IdeFileName, flags). The server writes the content to the selected file, then runs/bin/bash -c <Compiler script from demos.xml>in the same sandbox, with 3× the memory limit. - Fork a REPL and add terminal tabs. The window title carries a
jid, an encoded PID. Fork REPL and every extra tab open?jid=<id>, and the servernsenters the new shell into the parent's namespaces and working directory. This lets two terminals talk to each other, for example for socket programming. - Share a REPL. The owner's browser (the master) mirrors terminal output, language changes and file events to Firebase RTDB under
openrepl/<id>. A viewer who opens…/#<id>renders that stream, and their keystrokes are relayed to the master's WebSocket. The viewer never starts a REPL of their own. - Sign in. FirebaseUI, in a dialog over the home page (Google, GitHub or email, with email verification), signs the user in. The browser then posts the user to
/login. The server stores the session in the user database (user_sessions) and sets theuser-sessioncookie. Signed-in users get a stable home directory. Guest directories are deleted 60 minutes after last use. - Ask Genie or generate a practice question. The browser calls
/chat/completionswith a per-session access token. The server checks the origin, the token and a cookie-based rate limit, then forwards the request to OpenAI or OpenRouter with the server's API key.
The server's own data goes through one small interface, persist.Store (Store, Fetch, Delete, Each, Commit, Rollback, Close). Every database of the site is a key-value store behind it, so the rest of the code does not know where the data lives.
flowchart TB
subgraph APP["gotty (gateway or standalone server)"]
U["user, cookie<br/>accounts, sessions"]
H["server<br/>feedback, blog, shared code,<br/>practice progress"]
ST["server<br/>admin settings"]
C["cachedb<br/>15 MB read cache"]
P["persist.Store<br/>chosen once at start-up"]
SS["settings store<br/>one versioned document"]
end
U --> C --> P
H --> P
ST --> SS
P --> F[("MongoDB, first choice<br/>collections kv_*")]
P --> G[("Firestore, second choice<br/>collections kv_*")]
P --> L[("UnQLite files, the default<br/>/opt/gotty/*.db")]
SS --> F
SS --> G
SS --> J[("settings.json")]
Which backend. persist.Init chooses once, when the server starts, and the choice holds until it stops:
- MongoDB, if
OPENREPL_MONGODB_URIis set and answers (for example an Atlas cluster; the database isOPENREPL_MONGODB_DB, defaultopenrepl). - Otherwise Firestore, if
OPENREPL_FIRESTORE_CREDENTIALSholds a service account key (or the emulator is set) and the project's Firestore answers. The server uses Firestore's REST API directly, with no client library. - Otherwise UnQLite files under
/opt/gotty, as the server has always done.
Each backend is tried three times. A database that was asked for and did not answer is written to the log with the reason, the server goes on with the next choice, and the admin Health page shows a "Database choice" warning. Use MongoDB or Firestore on a host whose disk is wiped on every deploy (Render, for example): with files only, accounts, blog posts and settings are lost on each deploy.
The databases. With files, each is a file; with MongoDB or Firestore, each is a collection named after the file (user_sessions.db becomes kv_user_sessions).
| Database | Key → value | Used by |
|---|---|---|
user_sessions |
<uid> → the user's profile and login sessions (JSON); SESSION_KEY → the secret that signs the session cookie, when OPENREPL_SECRET is not set; blocked:<uid> → set by an admin; worker-pin:<uid> → the execution node that holds the user's workspace (distributed mode) |
sign-in, the session cookie, admin accounts page |
feedback |
<timestamp> → {Name, Email, Message, Read} |
/feedback, the admin inbox |
blog |
<post name> → the post (JSON) |
/blog, and Genie's knowledge of the site |
snippets |
<8-character id> → the shared code (JSON) |
/snippet, /s/<id> |
practice |
u:<uid> → the user's practice progress (JSON) |
/practice/progress |
A record is {_id: <key>, v: <value bytes>, t: <time>} in MongoDB, and a document with the fields v and t in Firestore. The first start with a remote database copies an existing file into its empty collection, once (the cookie secret included, so nobody is signed out). After that the remote database wins and the file is not read again.
The admin settings are one JSON document, not a key-value database: the colour of the day, the announcement, maintenance, switched-off languages (for the site and per node), Genie's switches, rates and model list, added admins, and the API keys an admin saved (encrypted with AES-256-GCM under the server's secret). It follows the same choice: the file settings.json, or the document site of the collection settings in MongoDB or Firestore. The document has a version, and a save succeeds only if the version has not moved, so two admins or two instances cannot overwrite each other. With MongoDB or Firestore every instance checks the version every 10 seconds and applies a change without a restart.
What always stays in files on the server's own disk: the admin audit log (admin-audit.jsonl), usage numbers (admin-stats.json), the job file that remembers which guest workspaces to delete, the logs, and the workspaces themselves (/tmp/home). The agent task counts and the Genie request balance are not stored at all: the first is in memory, the second in the visitor's signed cookie.
In distributed mode the gateway owns the databases and the settings. A worker never connects to MongoDB or Firestore, whatever its environment holds: it keeps only workspaces and takes its rules from the gateway (see Distributed mode).
What the browser keeps in Firebase, without the server in between: shared sessions and Genie chat history in the Realtime Database, and practice questions in the project's Firestore on the practice pages. If the server's databases are in the same Firestore, keep the security rules of kv_* and settings closed (allow read, write: if false): the server's service account bypasses rules, and a browser must not be able to read sessions or the encrypted keys.
Details: LLD 05, section 4 (stores, backends, data models) and LLD 13 (settings, saved keys, Health).
flowchart TB
U["Users"] --> CF["CDN / tunnel<br/>(e.g. Cloudflare)"]
CF --> H["Linux VM (cgroup v1 host)"]
subgraph H
D["Docker container<br/>--privileged, /sys/fs/cgroup mounted<br/>run_app.sh → gotty -w ..."]
S["or: systemd gotty.service<br/>(deb package, user gottyuser)"]
end
D --> V1[("/opt/gotty<br/>DB files, settings, audit log,<br/>jobfile, .gitconfig")]
D -. "optional" .-> M[("MongoDB or Firestore<br/>the databases and the settings")]
D --> V2[("/tmp/home<br/>workspaces")]
D --> V3[("/gottyTraces<br/>logs")]
- Image: the multi-stage
Dockerfilebuilds on Ubuntu 22.04.install_prerequisite.shinstalls every REPL toolchain, andmake allbuilds the binary. CI builds the image on every PR and push tomaster. Pushes tomasteralso publish:<sha>and:latest. - Sandboxing needs cgroup v1. With
--privilegedand/sys/fs/cgroupmounted, each REPL gets its own namespaces and memory cgroup. Without them, REPLs still run but share the container. - Server-side secrets come from the environment (see Settings and secrets). The older git-config file,
/opt/gotty/.gitconfig, still works as a fallback.
The server-side settings come from environment variables, and gotty can load them from an env file (--env-file, ~/.env by default). .env.example is a template: copy it, fill it in, and keep it out of git. See Loading .env.
| Variable | Meaning | Older file key |
|---|---|---|
OPENREPL_ADMIN_EMAILS |
Owner accounts, comma-separated. Owners are admins and the only ones who can add or remove other admins in the dashboard. With none set, nobody is an admin. | user.email |
OPENREPL_OPENAI_API_KEY |
The OpenAI key, as it is (not base64). | user.OpenaiAPIKey (base64) |
OPENREPL_MONGODB_URI |
Optional. Keeps the admin settings and every database (sessions, feedback, blog, snippets, practice) in MongoDB (for example an Atlas mongodb+srv:// URI) instead of files under /opt/gotty, so they survive deploys that wipe the disk. The first start copies the existing files in, once. A secret. Gateway and standalone only; a worker keeps files. |
|
OPENREPL_FIRESTORE_CREDENTIALS |
Optional. A Google service account key (the JSON, its base64, or a file path) for the Firebase project's Firestore. Used when OPENREPL_MONGODB_URI is not set and Firestore answers; otherwise the server uses files. Keep the security rules of kv_* and settings closed. |
|
OPENREPL_FIRESTORE_PROJECT |
The project, when the key does not name it (the emulator). | |
OPENREPL_MONGODB_DB |
The MongoDB database for it. Default openrepl. |
|
OPENREPL_SECRET |
Optional. A long random secret for the server's own use: it signs the session cookies and encrypts the API keys an admin saves in the dashboard (a key saved there wins over the two above). When set, it is not saved anywhere (the cookie key is derived from it); when not set, the server makes one and saves it in its database. A worker takes its gateway's. If it changes, everybody is signed out once and saved keys must be entered again. | |
OPENREPL_OPENROUTER_API_KEY |
Optional. The OpenRouter key, as it is. Without it the Gemma 4 31B choice is not offered. | |
OPENREPL_HOST |
The origin the chat proxy accepts. Default localhost. |
user.host |
OPENREPL_FIREBASE_CONFIG |
The Firebase project the page signs in with, as JSON or base64 of JSON. Default: the built-in production project. See below. | |
OPENREPL_ENV |
dev or production (the default). Dev shows the values of these settings in the start-up log; production only says which are set. |
The environment wins over the file. A variable that is empty counts as not set, so a half-filled .env does not switch the file off. The file is looked for in /opt/gotty/.gitconfig, then ~/.gitconfig, then /etc/.gitconfig, and only the first one that exists is read.
gotty loads an env file when it starts. The flag is --env-file, and it defaults to ~/.env:
gotty -w # reads ~/.env if there is one
gotty -w --env-file /etc/openrepl.env # reads that file insteadA file you name with --env-file (or GOTTY_ENV_FILE) has to exist and has to be readable, or gotty stops and says why. The default ~/.env may be missing, and then gotty simply goes on.
Rules for what is in the file:
- One
NAME=valueper line.#starts a comment,export NAME=valueis accepted, and a value can be unquoted, in'single quotes'(taken literally) or in"double quotes"(\n,\",\\and\$are understood). There is no$VARexpansion, and a value cannot run over more than one line. - Only
OPENREPL_*andGOTTY_*names are used. Anything else in the file is ignored and named in the log, so a~/.envthat other tools share cannot changePATHorLD_PRELOADfor the server.GOTTY_*means every gotty flag can be set in the file too:GOTTY_PORT=8081,GOTTY_WORKER_TOKEN=...and so on. - The real environment wins. A variable that is already set keeps its value, so
OPENREPL_ENV=production gottyoverrides the file. The order is: the environment, then the env file, then the git-config file. - It is read once. Edit the file and restart gotty.
- Keep it private.
chmod 600it. gotty warns in its log if other users can read it..envis in.gitignore; keep it out of git. - A bad line stops a named file and gotty reports the line number (never the text). In the default
~/.enva bad line is skipped with a warning.
To see what gotty picked up, look at the config: line it writes at start-up. It says which settings are set and where each comes from, what the env file did (how many loaded, kept or ignored; never the values), and with OPENREPL_ENV=dev it shows the values:
grep config: /gottyTraces/gotty.log | tail -1Where to keep the file depends on how you run gotty:
| How you run gotty | Where to keep the file | How it is loaded |
|---|---|---|
| Standalone on your own machine or a server | ~/.env, the default; or anywhere, with --env-file |
gotty reads it |
| The dev container | .env in the repo root. Inside the container ~ is /root, which is lost when the container stops, but the repo is mounted at /opt/openrepl, so the file survives |
../bin/gotty -w -p 8080 --env-file /opt/openrepl/.env |
| The Docker image | Outside the image, for example next to the repo | docker run --env-file .env ..., which is Docker's own flag and goes before the image name, or mount the file and pass gotty's --env-file after the image name |
| A systemd service | /etc/openrepl/openrepl.env, owned by root, chmod 600 |
EnvironmentFile=/etc/openrepl/openrepl.env in the unit, or --env-file on the ExecStart line |
Standalone, step by step:
cd ~/Documents/openrepl/openrepl # on your Mac: the repo root
cp .env.example ~/.env # once; edit it afterwards
chmod 600 ~/.env
cd src && ../bin/gotty -w -p 8080 # picks up ~/.envDocker and systemd read an env file themselves, with their own rules: docker run --env-file keeps quotes as part of the value and expands nothing, so give the Firebase config as base64 there (see below). systemd accepts quotes, but not export. Setting the variables by hand also works: set -a; . ./.env; set +a before starting gotty. set -a is what exports them; with only . ./.env gotty does not inherit them.
The REPLs do not get these settings. gotty starts every REPL without its own OPENREPL_* and GOTTY_* variables (which includes GOTTY_WORKER_TOKEN and GOTTY_CREDENTIAL), and without any variable whose name contains TOKEN, SECRET, PASSWORD, PASSWD, CREDENTIAL, API_KEY, APIKEY, ACCESS_KEY or PRIVATE_KEY. So env in a user's bash does not show them. The IDE's environment-variables box is expanded against that same filtered environment, so $OPENREPL_OPENAI_API_KEY typed in it comes out empty, while $PATH and $HOME work as before. This only controls what is passed on to the REPLs. How far a REPL is isolated from the server's files and processes is a separate matter; see LLD 03.
Sign-in goes through Firebase, so to test it without touching the production project, use a development project of your own:
-
In the Firebase console, add a project, for example
my-openrepl-dev. -
Authentication, then Sign-in method: enable Email/Password and Google, and GitHub if you want that button to work. Under Settings, then Authorized domains,
localhostis already listed. Add any other host you open the site on. Google sign-in only works onlocalhostor over HTTPS; email and password works on any listed host. -
Realtime Database: create a database (test mode is fine for development). The app uses it for shared sessions, and its URL is the
databaseURLbelow. -
Project settings, General, Your apps: add a Web app and copy the
firebaseConfigobject it shows you. -
Put it in your env file, and make your own address the admin.
OPENREPL_FIREBASE_CONFIGtakes the config in any of three forms:- The snippet exactly as the console shows it (
const firebaseConfig = { apiKey: "...", ... };, with or without the first line). Nothing in it is run; only thekey: "value"pairs are read. - JSON on one line:
{"apiKey":"AIza...","authDomain":"my-openrepl-dev.firebaseapp.com","projectId":"my-openrepl-dev","databaseURL":"https://my-openrepl-dev-default-rtdb.firebaseio.com"}. - The base64 of either, which has no quotes in it. The server's own built-in config is kept in this form, base64 of the snippet, so the same text works here. To make it, paste the snippet into a file and run
base64 < firebase-dev.txt | tr -d '\n'.
# ~/.env OPENREPL_ENV=dev OPENREPL_ADMIN_EMAILS=you@example.com OPENREPL_FIREBASE_CONFIG=eyJhcGlLZXkiOiJBSXphLi4uIiwiYXV0aERvbWFpbiI6Ii4uLiIsInByb2plY3RJZCI6Ii4uLiJ9In an env file that gotty reads, quotes around the value work (
'...'or"..."). Withdocker run --env-filethey would become part of the value, so use the base64 form there. - The snippet exactly as the console shows it (
-
Restart the server and open
http://localhost:8080. The users are those of the development project, so create your account there (or sign in with Google), and use the same address inOPENREPL_ADMIN_EMAILSto be the admin.
Only apiKey, authDomain and projectId are required, and fields that are not part of a Firebase web config are ignored. A wrong value stops the server at start-up and names the field, so a typo cannot quietly send a development machine to the production project. In dev mode the start-up log shows which project is in use.
| UI option | WebSocket path | Backend command | Memory limit (MB) |
|---|---|---|---|
| C / C++ | /ws_c, /ws_cpp |
cling (C adds -xc -noruntime) |
22 |
| Go / Go-yaegi | /ws_go, /ws_yaegi |
gointerpreter, yaegi |
45, 10 |
| Java | /ws_java: a tuned single-JVM jshell; Run compiles the file |
jshell (REPL), javac and java (Run) |
192 |
| JavaScript | iframe to jsconsole.html (runs in the browser) |
n/a | n/a |
| TypeScript, NodeJS | /ws_ts-node, /ws_node |
ts-node, node |
50, 10 |
| Python, Python2.7, IPython3 | /ws_python, /ws_python2.7, /ws_ipython3 |
same names | 2, 2, 20 |
| Ruby, Perl, Tcl, bash | /ws_irb, /ws_perli, /ws_tclsh, /ws_bash |
same names | 10, 3, 2, 10 |
| Rust, SQLite, JSON, Assembly x86 | /ws_evcxr, /ws_sqlite3, /ws_jq-repl, /ws_rappel |
same names | 50, 10, 2, 2 |
The memory limits come from Commands2memLimitMap. They double as admission-control weights: a new connection is refused when either the number of connections or the total weight exceeds --max-connection. See docs/lld/09-adding-a-repl.md to add a language.
GoTTY's original design notes still apply to the terminal core. It uses xterm.js and hterm, and the hterm + websocket idea was inspired by Wetty.
- gotty-client: If you want to connect to GoTTY server from your terminal
- Secure Shell (Chrome App): If you are a chrome user and need a "real" SSH client on your web browser, perhaps the Secure Shell app is what you want
- Wetty: Node based web terminal (SSH/login)
- ttyd: C port of GoTTY with CJK and IME support
- tmate: Forked-Tmux based Terminal-Terminal sharing
- termshare: Terminal-Terminal sharing through a HTTP server
- tmux: Tmux itself also supports TTY sharing through SSH)
The MIT License
