Skip to main content

WebSockets

Bidirectional real-time channels via the regius/ws package (built on gorilla/websocket), with the same ergonomics as the SSE broker: broadcast/send, bounded per-client buffers, heartbeats — and a {"event","data"} envelope shared with SSE, so one payload works over both transports unchanged.

Features​

  • Hub available on every app: app.WS — mount app.WS.Handler(upgrader) on any route, or use the default route
  • Default route at /ws is opt-in (WS_ENABLED), mounted on the outer mux like the SSE stream
  • JSON helper: app.WSBroadcastJSON(event, payload) — mirrors SSEBroadcastJSON
  • Handshake security by default: same-origin checks (cross-site WebSocket hijacking protection), WS_ALLOWED_ORIGINS to admit other origins, WS_ALLOW_EMPTY_ORIGIN=false to require browser clients
  • Per-connection care: heartbeat pings (30s) with pong-extended liveness deadlines, a 32 KiB message limit, bounded buffers with slow-client eviction, and an optional client cap
  • Upgrades work through the middleware stack — including behind the session middleware and under regius dev

Quickstart​

regius new demo && cd demo   # 1. create an app
regius make websocket chat # 2. scaffold the endpoint (handler + route at /ws/chat)
  1. Start the app — regius dev (hot-reload) or go run .. The socket is live immediately: the generated handler echoes whatever you send. No .env changes are needed for it
  2. Talk to it from a terminal:
websocat ws://localhost:4000/ws/chat
{"event": "ping", "data": "hello"}
# echoed straight back: {"event":"ping","data":"hello"}
  1. Or from the browser (a page served by the app — cross-origin pages are rejected by default):
const socket = new WebSocket("ws://localhost:4000/ws/chat");
socket.onmessage = (e) => console.log(JSON.parse(e.data));
socket.send(JSON.stringify({ event: "ping", data: "hello" }));
  1. Make it yours: dispatch on the event name inside the read loop of handlers/ws_chat.go, and push to every connected socket from any handler with handlers.WSBroadcastChat(h.App, "chat.message", payload)

Usage​

// Broadcast a JSON event to every connected socket (same envelope as SSE)
_ = app.WSBroadcastJSON("notification", map[string]string{
"message": "Hello, world!",
})

// Per-client operations
err := app.WS.Send(clientID, ev) // targeted send
clients := app.WS.Clients() // snapshot: id, remote addr, connected-at
_ = app.WS.Close(clientID, "server shutdown")

// An authenticated socket on an app route: the session cookie rides the
// GET handshake, so you can check the session before upgrading
r.Get("/ws/chat", func(w http.ResponseWriter, r *http.Request) {
conn, err := (&ws.Upgrader{}).Upgrade(w, r)
if err != nil {
return // the HTTP error response (403/400) was already written
}
defer conn.Close("done")
for {
var ev ws.Event
if err := conn.ReadJSON(&ev); err != nil {
return // client gone, malformed, or oversized message
}
_ = conn.WriteEvent(ev) // echo it back
}
})

Mounting Patterns​

MountWhereWhat runs on the handshake
Default route (WS_ENABLED=true)outer mux at WS_PATHRequestID, IPFilter, SecurityHeaders — bypasses session/CSRF/sanitizer/maintenance
App route (what regius make websocket scaffolds)r.Routes at /ws/<name>the full app stack — the session is loaded, so authenticate before upgrading

Middleware Notes​

  • The upgrade hijacks the connection, so body-modifying middleware steps aside: DevReload skips script injection, and the session middleware's cookie writer is bypassed
  • If you override CONTENT_SECURITY_POLICY, include connect-src ws://localhost:* wss://your-domain — otherwise browsers refuse the socket
  • IPFilter and the rate limiter apply at handshake time, useful for blocking abusive clients before the socket opens

Reverse Proxy (wss://)​

Terminate TLS at the proxy and forward the upgrade headers — nginx example:

location /ws/ {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s; # keep above WS_PONG_TIMEOUT or the proxy idle-kills the socket
}

Environment Variables​

WS_ENABLED=false
WS_PATH=/ws
WS_ALLOWED_ORIGINS=
WS_ALLOW_EMPTY_ORIGIN=true
WS_HEARTBEAT=30s
WS_WRITE_TIMEOUT=10s
WS_PONG_TIMEOUT=60s
WS_MAX_MESSAGE_SIZE=32768
WS_CLIENT_BUFFER=16
WS_MAX_CLIENTS=0