πŸ–₯️ 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.