FluxyChat

Reference

Pusher Channels parity

How FluxyChat maps Pusher Channels concepts onto rooms, JWT auth, Durable Objects, and the SDK.

FluxyChat maps Pusher Channels concepts onto rooms, JWT auth, Cloudflare Durable Objects, and @fluxy-chat/sdk. Use this page when migrating from Pusher or comparing event models.

Concept mapping

PusherFluxyChat
ChannelRoom (rooms.id)
Public channelRoom type: "public"
Private channelRoom type: "group" or dm plus membership
Presence channelWS presence events on any room
Cache channelcache=1 on WS plus DO snapshot
Encrypted channele2eEnabled on room plus SDK AES-GCM
User channelUserDurableObject plus /ws/user/:userId
socket_idsocketId on subscribe; excludeSocketId on publish
HTTP triggerPOST /events (multi-room)
WebhooksOutbound signed webhooks plus verify playground

Authentication is always JWT (or project API key for server routes). Public rooms do not mean anonymous access: any signed-in project member may join.

Publish, subscribe, and auth

  • WebSocket: connect to /ws/rooms/:roomId?token=…
  • REST: messages, edits, reactions via standard room APIs
  • Auth: JWT with projectId, userId, roles; membership enforced in Worker and Room DO

Presence and subscription count

WebSocket events (SDK types on FluxyChatClient):

  • subscription_succeeded: initial presence snapshot
  • member_joined / member_left
  • presence: full member list updates
  • subscription_count: connection count for the room

Pass optional presenceInfo on connect for user_info-style metadata.

Occupied and vacated webhooks

Configure outbound webhooks for:

  • room.occupied: first subscriber connects
  • room.vacated: last subscriber disconnects

Defaults are available in the admin console webhook UI.

Client events

Clients may send client-* event names over WS (rate-limited). Server broadcasts to other members and can emit client_event webhooks. Use excludeSocketId to skip the sender.

Cache channels

  • Connect with cache=1 query param to receive the last cached server event immediately
  • Room DO stores a small snapshot; cache_miss webhook when cache is empty on subscribe

Encrypted channels (E2E)

  1. Owner or admin: PATCH /rooms/:id with { "e2eEnabled": true }
  2. Members: GET /rooms/:id/e2e-key returns { e2eKey } (server-wrapped at rest)
  3. SDK: getRoomE2eKey(roomId) and encrypted message envelopes (room-e2e.ts)

TLS still applies; E2E protects payload at the application layer.

AspectPusher encrypted channelsFluxyChat room E2E
Key distributionClient derives shared secret; Pusher never sees plaintextRoom key via GET /rooms/:id/e2e-key (JWT member); key wrapped at rest in D1
Server visibilityPusher routes ciphertext onlyWorker and DO store ciphertext envelopes
Channel authPOST /auth returns signed subscriptionPOST /auth/channel plus member JWT; optional presenceInfo
RotationApp-managed master secretRe-enable E2E on room or rotate via admin PATCH /rooms/:id

Manual E2E checklist:

  1. Enable E2E on a private room; two members fetch key and exchange encrypted messages. A third member without key sees opaque payload in REST and WS.
  2. Disable E2E: new messages are plaintext on the wire (TLS only).
  3. Guest session on a public room must not call /e2e-key (403) unless promoted to member.

Pusher docs: Encrypted channels. FluxyChat uses the room key API and SDK room-e2e.ts helpers instead of Pusher shared-secret derivation.

User channel

  1. POST /auth/signin: exchange credentials for a user-scoped session (see OpenAPI)
  2. SDK: signIn(), connectUser(), triggerUserEvent(userId, event)
  3. React: useUserChannel({ userId })
  4. Server push: POST /users/:userId/events (JWT, same user or admin)

Binding: USER Durable Object in wrangler.toml.

Multi-room HTTP trigger

curl -X POST "$WORKER/events" \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "roomIds": ["room-a", "room-b"],
    "name": "my-event",
    "data": { "score": 1 },
    "excludeSocketId": "abc123"
  }'

SDK: client.triggerEvents({ roomIds, name, data, excludeSocketId })

Exclude sender

On WS connect, server assigns socketId. Pass excludeSocketId (or socket_id) on POST /events and room announce paths to omit the originating connection.

Connection state

SDK emits state_change and exposes connectionState (connecting, connected, reconnecting, disconnected, and so on).

Transport fallbacks

Order: WebSocket, then SSE (/rooms/:id/stream), then polling. See transport fallback.

Webhook batch verification

Verify inbound or test signatures:

  • Single payload: POST /webhooks/verify (registered webhook) or POST /webhooks/verify-batch with { secret, body, signature }
  • Batch: { secret, events: [...], signature } where signature is HMAC-SHA256(secret, JSON.stringify(events))
  • Headers: X-Fluxy-Signature or X-Pusher-Signature (alias)

Admin Webhook playground supports registered, raw, and batch modes.

Live inspector

Admin console includes a realtime event inspector for debugging WS traffic.

Watchlist

  • GET/POST/DELETE /users/:userId/watchlist
  • Fanout: room events matching watchlist targets emit watchlist_event on user channel
  • Dashboard: user watchlist card

Migration: 0042_user_watchlist.sql

Terminate connections

DELETE /users/:userId/connections closes user channel WS and room connections for that user.

SDK: terminateUserConnections(userId)

Global event binding

client.onAnyEvent((type, payload) => { /* bind_global */ });
client.offAnyEvent(handler);
// useChat({ onAnyEvent: ... })

Public channels

  • Create room with type: "public" (or patch existing room)
  • Any authenticated project member may connect; first connect lazy-joins room_members
  • Optional stricter limit: RATE_LIMIT_PUBLIC_WS_CONNECTIONS_PER_MINUTE

Webhook event types

Register client_event, member_joined, member_left, subscription_count, cache_miss, room.occupied, room.vacated, and related types in webhook configuration. Member and subscription webhooks are emitted from Room DO when those WS events occur.

SDK quick reference

import { FluxyChatClient, useChat, useUserChannel } from "@fluxy-chat/sdk";

const client = new FluxyChatClient({ baseUrl, userId, token });

await client.signIn({ /* … */ });
const userConn = client.connectUser();
userConn.on("notification", (p) => {});

await client.triggerEvents({
  roomIds: ["a", "b"],
  name: "score-update",
  data: { n: 1 },
  excludeSocketId: conn.socketId,
});

const { e2eKey } = await client.getRoomE2eKey(roomId);
await client.addWatchlistTarget(userId, { type: "room", targetId: roomId });
client.onAnyEvent((type, data) => console.log(type, data));
await client.terminateUserConnections(userId);
const { messages, connectionState } = useChat({
  roomId,
  client,
  onAnyEvent: (type, data) => {},
  cache: true,
});

Deploy checklist

  1. Apply D1 migrations through 0042 (E2E, watchlist, and related tables)
  2. Deploy Worker with USER binding and DO migration v3
  3. Set optional RATE_LIMIT_PUBLIC_WS_CONNECTIONS_PER_MINUTE in Worker secrets
  4. Redeploy dashboard for admin inspector and webhook playground

OpenAPI: apps/worker/openapi.yaml documents /events, /auth/signin, /users/*, /webhooks/verify-batch, and /rooms/{id}/e2e-key.

On this page