# Socket Broker A Socket.IO broker with topic-based messaging for ComedyKit. Backend services publish over an HTTP API protected by an API key; browsers and mobile clients authenticate with short-lived, topic-scoped tokens issued by Laravel. ## Features - Socket.IO server for real-time client subscriptions. - HTTP API for backend publishing. - API-key authentication for publishers. - HMAC-SHA256 client tokens for connection and topic authorization. - Room-based topic delivery (no in-memory subscription maps). - Protected topic introspection and public health check. - CORS allowlist, subscription limits, and subscribe rate limiting. ## Architecture ``` Frontend / mobile clients │ Socket.IO (connection token + topic token) ▼ Socket broker ── rooms: user/{uuid}/notifications ▲ event/{slug}/ticket_statistics │ POST /publish (API key) Laravel backend ``` ## Configuration Copy `env.example` to `.env`: | Variable | Default | Description | | --- | --- | --- | | `WS_PORT` | `3000` | Socket.IO / HTTP port | | `API_PORT` | `WS_PORT` | Optional second HTTP API port | | `API_KEY` | `default-api-key-change-me` | Publisher API key; never expose to clients | | `SOCKET_TOKEN_SECRET` | — | Shared HMAC secret; must match Laravel `SOCKET_TOKEN_SECRET` | | `AUTH_MODE` | `required` | `off`, `optional` (rollout only), or `required` | | `ALLOWED_ORIGINS` | `https://*.comedykit.be,...` | Comma-separated origins; supports `*.domain.tld` | | `MAX_TOPICS_PER_SOCKET` | `20` | Subscription cap per connection | | `SUBSCRIBE_RATE_LIMIT` | `30` | Subscribe attempts per 10 seconds per connection | `AUTH_MODE=required` refuses to start without `SOCKET_TOKEN_SECRET`. ### Rollout 1. Deploy with `AUTH_MODE=optional` so legacy clients still work. 2. Deploy Laravel with `/notifications/socket-token` and the new publisher job. 3. Deploy the updated web/mobile clients. 4. Switch to `AUTH_MODE=required`. ## Client authentication Laravel issues two token types: ```json // Connection token { "v": 1, "sub": "", "typ": "connect", "iat": 0, "exp": 0, "jti": "..." } // Topic token { "v": 1, "sub": "", "typ": "topic", "topic": "event/slug/ticket_statistics", "iat": 0, "exp": 0, "jti": "..." } ``` Tokens are encoded as: ``` base64url(JSON payload) + "." + base64url(HMAC-SHA256(payload, SOCKET_TOKEN_SECRET)) ``` Clients connect with the connection token: ```javascript const socket = io("https://bram-sockets.comedykit.be", { auth: { token: connectionToken }, }); ``` A user may always subscribe to `user/{their-own-id}/notifications`. Any other topic requires a topic token whose `sub` matches the connection subject and whose `topic` matches the subscription exactly. ## WebSocket API ### Subscribe ```javascript socket.emit("subscribe", "user//notifications"); // Or, for a topic-scoped token: socket.emit("subscribe", { topic: "event/my-event/ticket_statistics", token: topicToken, }); socket.on("subscribed", data => { console.log(`Subscribed to ${data.topic}`); }); ``` ### Unsubscribe ```javascript socket.emit("unsubscribe", "user//notifications"); socket.on("unsubscribed", data => { console.log(`Unsubscribed from ${data.topic}`); }); ``` ### Receive messages ```javascript socket.on("message", data => { console.log(data.topic, data.message, data.data, data.timestamp); }); ``` ### Errors ```javascript socket.on("error", error => { // INVALID_TOPIC | FORBIDDEN_TOPIC | RATE_LIMITED | TOPIC_LIMIT console.error(error.code, error.message); }); ``` ## HTTP API ### Public - `GET /health` — status, connection count, auth mode. Does not expose topic names. ### Protected (API key) - `GET /topics` - `GET /topics/:topic` - `POST /publish` - `POST /publish/:topic` API key is accepted as `Authorization: Bearer `, `x-api-key`, or `api_key` query parameter. #### Publish ```bash curl -X POST http://localhost:3000/publish \ -H "x-api-key: your-api-key" \ -H "Content-Type: application/json" \ -d '{"topic":"user//notifications","message":"{\"id\":\"...\"}"}' ``` Response: - `200` with `{ success, message, subscribers, payload }` - `204` when the topic has no subscribers - `400` invalid request - `401` invalid or missing API key ## Development ```bash npm install npm test AUTH_MODE=optional API_KEY=dev-key SOCKET_TOKEN_SECRET=dev-secret npm start ``` ## Docker ```bash cp env.example .env # set API_KEY, SOCKET_TOKEN_SECRET, AUTH_MODE, ALLOWED_ORIGINS docker compose up -d ``` The Docker image runs as a non-root user and exposes port 3000. If `API_PORT` differs from `WS_PORT`, a second HTTP server is started on that port. ## Security notes - `API_KEY` is publisher-only. Never ship it to clients. - `SOCKET_TOKEN_SECRET` must be at least 32 random bytes and identical in Laravel and the broker. - Tokens are short-lived. The broker verifies them when a subscription is created; clients re-fetch tokens on reconnect. - Use HTTPS/WSS in production. - Rotate secrets by coordinating Laravel and broker deployments; outstanding tokens become invalid. - `AUTH_MODE=optional` or `off` is for local development or the rollout window only.