🖥️ 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/domWS 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/