kemal-websocket
kemalcr/kemal/.github/skills/kemal-websocket/SKILL.md
Implementing real-time bi-directional communication with WebSockets in Kemal, origin security, and lifecycle management.
Skill3.9k starsChanged 30 days ago
What's in it
- Kemal WebSocket Integration
- Version Notes
- Core Mandates
- Patterns from Source Code
- Centralized WebSocket Hub (real-time-dashboard/src/services/dashboardhub.cr)
- WebSocket Route Setup with Origin Security
- Best Practices
- When to Use
---
name: kemal-websocket
description: Implementing real-time bi-directional communication with WebSockets in Kemal, origin security, and lifecycle management.
license: MIT
---
# Kemal WebSocket Integration
This skill provides expert guidance on using WebSockets for real-time features in Kemal, with version-aware origin security guidance, strictly following patterns from [`kemal-by-example/real-time-dashboard`](https://github.com/sdogruyol/kemal-by-example/tree/master/real-time-dashboard) and [`kemal-by-example/twitter-clone`](https://github.com/sdogruyol/kemal-by-example/tree/master/twitter-clone).
## Version Notes
- **Default Origin policy changed in 1.13.0:** with an empty `websocket_allowed_origins` (the default), Kemal 1.13.0+ enforces same-origin and rejects missing or mismatched `Origin` headers with `403 Forbidden`; releases up to 1.12.0 allow **all** origins (CSWSH risk — configure an allowlist explicitly, see below).
- `websocket_allowed_origins = ["*"]` opt-in allow-all: since Kemal 1.13.0.
- Non-GET upgrade rejection (RFC 6455 §4.1, `405 Method Not Allowed` + `Allow: GET`) and explicit `Connection: close` after a rejected handshake: since Kemal 1.13.0.
## Core Mandates
- **WebSocket Route Definition:** Use the `ws` helper to define WebSocket endpoints:
```crystal
ws "/socket" do |socket, env|
# Socket lifecycle callbacks
end
```
- **Origin Validation & CSWSH Security:**
- **CRITICAL ON STABLE RELEASES (1.12.0 and earlier):** An empty `Kemal.config.websocket_allowed_origins = [] of String` (default) **permits all origins** (`return true if allowed.empty?`).
To protect against Cross-Site WebSocket Hijacking (CSWSH) in production on these releases, you **MUST explicitly configure allowed origins**:
```crystal
# MANDATORY on 1.12.0 and earlier to protect against CSWSH:
Kemal.config.websocket_allowed_origins = %w[https://myapp.com http://localhost:3000]
```
- **ON KEMAL 1.13.0+:** An empty `websocket_allowed_origins` defaults to strict **same-origin** enforcement (matching request `Host`). Missing or mismatched `Origin` headers are rejected with `403 Forbidden`. To explicitly allow all origins:
```crystal
# Kemal 1.13.0+ — opt-in allow-all:
Kemal.config.websocket_allowed_origins = ["*"]
```
- **Protocol Compliance (RFC 6455 §4.1) — *[Kemal 1.13.0+]*:**
- On Kemal 1.13.0+, WebSocket upgrade handshakes require the `GET` method. Non-GET upgrade requests (`POST`, `QUERY`, etc.) are automatically rejected with `405 Method Not Allowed`, `Allow: GET`, and `Connection: close`.
- Failed handshakes close the connection immediately to prevent WebSocket connection smuggling.
- **Socket Lifecycle Callbacks:**
- `socket.on_message`: Handle incoming text messages from client
- `socket.on_close`: Handle socket disconnection and perform cleanup
- `socket.on_ping` / `socket.on_pong`: Handle heartbeat signals
- **Concurrency Safety:** Always protect shared socket collections with a `Mutex`:
```crystal
class Hub
@sockets = Array(HTTP::WebSocket).new
@mutex = Mutex.new
def register(socket)
@mutex.synchronize { @sockets << socket }
end
def unregister(socket)
@mutex.synchronize { @sockets.delete(socket) }
end
end
```
- **Error Handling:** Rescue `IO::Error | Socket::Error` when sending to sockets and unregister closed sockets.
## Patterns from Source Code
### Centralized WebSocket Hub (real-time-dashboard/src/services/dashboard_hub.cr)
```crystal
require "kemal"
module RealTimeDashboard
class DashboardHub
# NOTE: Do not `include JSON::Serializable` here — it removes the default
# constructor, and sockets/mutexes are not serializable anyway. Serialize a
# plain stats payload (e.g. `{"total_requests" => total_requests}.to_json`)
# when broadcasting instead.
getter total_requests : Int64 = 0_i64
getter active_users : Int32 = 0
@sockets = Array(HTTP::WebSocket).new
@mutex = Mutex.new
def register(socket : HTTP::WebSocket)
@mutex.synchronize { @sockets << socket }
end
def unregister(socket : HTTP::WebSocket)
@mutex.synchronize { @sockets.delete(socket) }
end
def broadcast(message : String)
@mutex.synchronize do
@sockets.reject! do |socket|
begin
socket.send(message)
false
rescue IO::Error | Socket::Error
true # Remove failed socket
end
end
end
end
end
end
```
### WebSocket Route Setup with Origin Security
```crystal
require "kemal"
# Configure allowed origins explicitly (mandatory on 1.12.0 and earlier for security, recommended everywhere)
Kemal.config.websocket_allowed_origins = %w[http://127.0.0.1:3000 http://localhost:3000]
HUB = RealTimeDashboard::DashboardHub.new
ws "/dashboard/socket" do |socket, env|
HUB.register(socket)
socket.on_message do |msg|
# Handle incoming client messages (spawn long-running work to avoid blocking fiber)
spawn do
# process msg
end
end
socket.on_close do
HUB.unregister(socket)
end
end
```
## Best Practices
- **Security First:** Always configure `Kemal.config.websocket_allowed_origins` explicitly in production. Origin validation is a browser boundary, not authentication/authorization.
- **Mutex Synchronization:** Wrap all modifications and iterations over socket lists in `@mutex.synchronize`.
- **Selective Rejection:** Use `@sockets.reject!` inside broadcast loops to cleanly strip dead sockets without deadlocks.
- **Graceful Teardown:** Unregister sockets in `on_close` callbacks.
## When to Use
- When building real-time bi-directional chat applications, collaborative tools, or active telemetry dashboards.
- When client and server communicate via continuous socket connections.
More agent context in kemalcr/kemal
11 other files this repository gives its agents.
Skill
- crystal-kemal.github/skills/crystal-kemal/SKILL.md
- kemal-auth.github/skills/kemal-auth/SKILL.md
- kemal-core.github/skills/kemal-core/SKILL.md
- kemal-database.github/skills/kemal-database/SKILL.md
- kemal-json.github/skills/kemal-json/SKILL.md
- kemal-middleware.github/skills/kemal-middleware/SKILL.md
- kemal-oauth.github/skills/kemal-oauth/SKILL.md
- kemal-orm.github/skills/kemal-orm/SKILL.md
- kemal-sse.github/skills/kemal-sse/SKILL.md
- kemal-upload.github/skills/kemal-upload/SKILL.md
- kemal-view.github/skills/kemal-view/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

