# Kemal Websocket

> Implementing real-time bi-directional communication with WebSockets in Kemal, origin security, and lifecycle management.

- Skill: `kemalcr/kemal-websocket` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kemalcr/kemal-websocket`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kemalcr/kemal-websocket/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- License: MIT
- Author: kemalcr (https://skillmd.com/u/kemalcr)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kemalcr/kemal-websocket

---


# 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.

