π₯οΈ 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
| Service | Port | systemd | Purpose |
|---|---|---|---|
funday-terminal | 7681 (localhost only) | funday-terminal.service | WSβPTY bridge |
| nginx | 443 (public) | nginx | TLS + WS proxy at /terminal-ws/ |
| SvelteKit frontend | 3000 | funday-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
| Type | Purpose | Example |
|---|---|---|
create | Spawn PTY | {"type":"create","cols":100,"rows":30} |
input | Keystrokes | {"type":"input","data":"ls\n"} |
resize | Resize | {"type":"resize","cols":120,"rows":40} |
kill | Kill PTY | {"type":"kill"} |
Bridge β Browser
| Type | Fields | When |
|---|---|---|
created | pid | PTY spawned |
output | data (ANSI string) | Shell output (streaming) |
exit | code, signal | Shell exited |
error | message | Server 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:7681only - 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-terminalNginx
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/fundayis a file copy, not a symlink. After editingsites-available/funday, you MUST:sudo cp /etc/nginx/sites-available/funday /etc/nginx/sites-enabled/funday sudo nginx -t && sudo systemctl reload nginxVerify with:
sudo nginx -T 2>/dev/null | grep terminal-ws
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 502 Bad Gateway | Nginx stale file or bridge down | cp sites-available β sites-enabled, restart bridge |
| βDisconnectedβ in status bar | WS canβt reach bridge | Check: systemctl is-active funday-terminal |
| Blank terminal | @wterm/dom not in build | Rebuild: bash scripts/build-atomic.sh |
| Connection rejected (1013) | Max connections (default 4) | Wait or increase TERMINAL_MAX_CONN |
| No shell output | PTY not created | Send {"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/domimport β must beawait import()inonMount, never static (crashes SSR) wterm.destroy()β call inonDestroyor WASM leaks- nginx reload β pick up edits if
sites-enabled/fundayis stale β alwayscpfromsites-available/