๐Ÿ–ฅ๏ธ Web Terminal (wterm.dev)

Browser-based terminal giving a real bash shell on the server. Accessible at https://funday.gg/dev/terminal for dev-gated users.

Architecture

Browser (wterm WASM emulator)
  โ†•  WebSocket (JSON, wss://)
nginx  (/terminal-ws/ โ†’ 127.0.0.1:7681)
  โ†•  ws://
terminal-bridge.mjs  (node-pty + ws, port 7681)
  โ†•  PTY
/bin/bash -l  (login shell, Funday user)

Services

ServicePortsystemdPurpose
funday-terminal7681 (localhost only)funday-terminal.serviceWSโ†”PTY bridge
nginx443 (public)nginxTLS + WS proxy at /terminal-ws/
SvelteKit frontend3000funday-frontend.service/dev/terminal page

File Map

server/
โ””โ”€โ”€ terminal-bridge.mjs              โ† WSโ†”PTY bridge (ESM module, node-pty + ws)

etc/systemd/system/
โ””โ”€โ”€ funday-terminal.service          โ† systemd unit (enabled, restart-on-failure)

etc/nginx/sites-available/
โ””โ”€โ”€ funday                           โ† contains location ^~ /terminal-ws/ { ... }

frontend/src/
โ”œโ”€โ”€ lib/components/dev/
โ”‚   โ””โ”€โ”€ WTermTerminal.svelte         โ† wterm.dev Svelte 5 wrapper component
โ”œโ”€โ”€ routes/dev/terminal/
โ”‚   โ””โ”€โ”€ +page.svelte                 โ† /dev/terminal route page
โ”œโ”€โ”€ lib/config/
โ”‚   โ””โ”€โ”€ devTools.ts                  โ† sidebar registry (terminal entry)
โ””โ”€โ”€ types/
    โ””โ”€โ”€ wterm.d.ts                   โ† TypeScript declarations for @wterm/dom

WS Protocol

JSON frames over WebSocket. Each message: {"type": "...", ...}

Browser โ†’ Bridge

TypePurposeExample
createSpawn PTY{"type":"create","cols":100,"rows":30}
inputKeystrokes{"type":"input","data":"ls\n"}
resizeResize{"type":"resize","cols":120,"rows":40}
killKill PTY{"type":"kill"}

Bridge โ†’ Browser

TypeFieldsWhen
createdpidPTY spawned
outputdata (ANSI string)Shell output (streaming)
exitcode, signalShell exited
errormessageServer error

Security

  • Dev access gate โ€” /dev/* requires Nakama auth + developer role
  • Max 4 concurrent PTYs โ€” configurable via TERMINAL_MAX_CONN
  • Non-root โ€” User=usr, NoNewPrivileges=true, ProtectSystem=strict
  • Private port โ€” bridge binds 127.0.0.1:7681 only
  • TLS terminated at nginx โ€” wss:// over the wire
  • No shell escape โ€” wterm is render-only, keystrokes via WS bridge

Operations

# Service
sudo systemctl status funday-terminal
sudo systemctl restart funday-terminal
sudo journalctl -u funday-terminal -f
 
# Health
curl -s http://127.0.0.1:7681/ | python3 -m json.tool
 
# Change max connections
sudo systemctl edit funday-terminal  # add Environment=TERMINAL_MAX_CONN=8
sudo systemctl daemon-reload && sudo systemctl restart funday-terminal

Nginx

The /terminal-ws/ proxy block in sites-available/funday:

location ^~ /terminal-ws/ {
    proxy_pass http://127.0.0.1:7681/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 86400;
    proxy_send_timeout 86400;
}

โš ๏ธ sites-enabled/funday is a file copy, not a symlink. After editing sites-available/funday, you MUST:

sudo cp /etc/nginx/sites-available/funday /etc/nginx/sites-enabled/funday
sudo nginx -t && sudo systemctl reload nginx

Verify with: sudo nginx -T 2>/dev/null | grep terminal-ws

Troubleshooting

SymptomCauseFix
502 Bad GatewayNginx stale file or bridge downcp sites-available โ†’ sites-enabled, restart bridge
โ€Disconnectedโ€ in status barWS canโ€™t reach bridgeCheck: systemctl is-active funday-terminal
Blank terminal@wterm/dom not in buildRebuild: bash scripts/build-atomic.sh
Connection rejected (1013)Max connections (default 4)Wait or increase TERMINAL_MAX_CONN
No shell outputPTY not createdSend {"type":"create","cols":80,"rows":24}

Gotchas

  • class: directive + Tailwind / โ€” Svelte parser treats / as division. Use inline ternary: class="bg-{status === 'ok' ? 'success' : 'error'}"
  • Dynamic @wterm/dom import โ€” must be await import() in onMount, never static (crashes SSR)
  • wterm.destroy() โ€” call in onDestroy or WASM leaks
  • nginx reload โ‰  pick up edits if sites-enabled/funday is stale โ€” always cp from sites-available/

0 items under this folder.