@fluxy-chat/sdk
v0.4.2
Published
Fluxychat JavaScript/TypeScript SDK — WebSocket rooms, REST messages, FluxyRealtimeProvider, useChat with loadMore
Maintainers
Readme
@fluxy-chat/sdk
Client for a Fluxychat Worker (self-hosted or Fluxychat Cloud): rooms, messages, WebSockets, agents, and optional React useChat.
The SDK talks to your Worker URL. It does not include LLM API keys — only your Fluxy project API key or member JWT.
Install
npm install @fluxy-chat/sdk zustand
# or
pnpm add @fluxy-chat/sdk zustandPeer dependencies: install react (18+) when you use useChat / FluxyRealtimeProvider, and zustand (5+) for useChat, createFluxyRoomSession, or the room store helpers. The SDK has no runtime npm dependencies — only your Worker URL is contacted over the network (Socket network-access note).
Vanilla store (Vue, Solid, Node)
import { createFluxyRoomSession } from "@fluxy-chat/sdk";
const { store, stop } = createFluxyRoomSession({
roomId: "my-room",
client,
});
const unsub = store.subscribe((state) => {
console.log(state.messages, state.connectionState);
});
store.getState().sendMessage("hello");
store.getState().sendMessage("", null, undefined, {
templateId: "tpl_…",
templateVars: { name: "Ada" },
});
// cleanup: unsub(); stop();What you must configure
| Piece | Who sets it | Notes |
|--------|-------------|--------|
| Worker baseUrl | You | e.g. https://fluxychat-worker.<account>.workers.dev or your custom domain |
| Project API key (fc_…) | Worker / console | Mint JWTs via POST /auth/token with header X-Fluxy-Api-Key — server-side only |
| Member JWT | Your backend or Fluxychat console | Passed to FluxyChatClient / useChat as token |
| LLM provider keys | Worker secrets or console | For agents; never embed in the npm package |
Minimal backend (mint JWT)
curl -X POST "$WORKER_URL/auth/token" \
-H "Content-Type: application/json" \
-H "X-Fluxy-Api-Key: fc_your_project_key" \
-d '{"userId":"alice","roles":["member"],"ttlSeconds":3600}'Use the returned token in the browser.
Quick start (React)
Quick smoke test run this before writing SDK code
# Set these in your shell or .env.local first.
export WORKER_URL=https://your-worker.example.workers.dev
export JWT=eyJ... # member JWT from /auth/token
export ROOM_ID=general
# 1. Send a message
curl -sS -X POST "$WORKER_URL/messages" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d "{\"roomId\":\"$ROOM_ID\",\"content\":\"hello from the smoke test\"}"
# 2. Read it back
curl -sS "$WORKER_URL/api/messages?roomId=$ROOM_ID&limit=1" \
-H "Authorization: Bearer $JWT"
# Verified against @fluxy-chat/[email protected] 2026-06-26If both calls return 200 and the message id round-trips, your Worker URL, JWT, and room are wired correctly. Skip to Option A below.
Option A — explicit client
import { FluxyChatClient, useChat } from "@fluxy-chat/sdk";
const client = new FluxyChatClient({
baseUrl: process.env.NEXT_PUBLIC_FLUXYCHAT_WORKER_URL!,
userId: "alice",
token: memberJwtFromYourBackend,
});
function Room({ roomId }: { roomId: string }) {
const {
messages,
sendMessage,
connectionState,
retryMessage,
loadMore,
hasMore,
isLoadingMore,
} = useChat({ roomId, client, markReadLatest: true });
// connectionState.nextRetryAt → "Reconnecting in 3s…"
// messages[].deliveryStatus → pending | sent | failed
// render messages; call loadMore() when the user scrolls to the top
}
// Verified against @fluxy-chat/[email protected] 2026-06-26Room catch-up + drafts
const catchUp = await client.getRoomCatchUp(roomId);
// { unreadCount, lastReadMessageId, firstUnreadMessageId }
await client.putRoomDraft(roomId, { content: "…", replyToId: null });
const draft = await client.getRoomDraft(roomId);
// Ephemeral message (1 hour TTL)
await client.createMessage(roomId, "self-destructs", null, undefined, undefined, {
expiresInSeconds: 3600,
});Rooms unread + notifications
const rooms = await client.listRooms(); // may include unreadCount per room
const notes = await client.listNotifications({ unreadOnly: true });
await client.markNotificationRead(notes[0].id);
// useNotifications(client) — React hook for in-app notification inboxOption B — FluxyRealtimeProvider (hosted Next.js or custom mint)
Wrap your app (or chat layout) once. The provider refreshes the member JWT before expiry and on auth errors.
import { FluxyRealtimeProvider, useChat } from "@fluxy-chat/sdk";
export function ChatLayout({ children }: { children: React.ReactNode }) {
return (
<FluxyRealtimeProvider
workerUrl={process.env.NEXT_PUBLIC_FLUXYCHAT_WORKER_URL!}
connectUrl="/api/fluxy/connect"
>
{children}
</FluxyRealtimeProvider>
);
}
function Room({ roomId }: { roomId: string }) {
const { messages, sendMessage, loadMore, hasMore } = useChat({ roomId });
// …
}For your own backend mint flow, pass authTokenProvider={() => fetch("/api/chat-token").then(r => r.json())} instead of connectUrl.
Pagination
GET /api/messages supports a before cursor (createdAt of the oldest visible message). The SDK sorts history chronologically and exposes:
hasMore— another page may existisLoadingMore—loadMore()in flightloadMore()— prepends older messages
Live snapshot (getParticipants)
GET /rooms/:id/live returns the room's Pusher-style channel stats plus a members: [{ userId, userInfo }] array (aggregated across shards for supergroup rooms, P10-SB8). Use it from the SDK to know "who's connected right now" without waiting for the next presence WS event:
const live = await client.getRoomLive("lobby");
// { occupied, online, subscriptionCount, userCount, users[], members: [{ userId, userInfo }], socketIds[] }
// Just the participants slice
const participants = await client.getRoomParticipants("lobby");
// React hook exposes a live snapshot in the store
const { live, loadLive } = useChat({ roomId: "lobby" });
await loadLive(); // refresh on demanduserInfo reflects whatever was passed in the WS presenceInfo query (or the JWT name / agent profile). For agent-typed participants, userInfo.agentId is set; consumers can branch on that.
Connection resilience
connectRoom() (used by useChat / createFluxyRoomSession) includes:
- Exponential backoff reconnect —
connectionState.status,nextRetryAt,reconnectAttempt - Outbound queue —
sendJsonqueues whileconnectingorreconnecting(cap 100 frames, 5 min TTL); flush on open. Inspect withgetOutboundQueueDepth(). - Heartbeat — client sends
{ type: "ping" }every 25s by default; server replies{ type: "pong" }. Disable withheartbeatIntervalMs: 0onconnectRoomoptions. - WS connect snapshot —
replay=connect+replayLimitquery params; server sends{ type: "replay", messages }(orhistoryby default). Setreplay: "off"viaconnect(roomId, { replay: "off" })for heavy rooms. - REST history replay after reconnect only if no WS snapshot arrived first (
replayHistoryOnReconnect, default true) - Streaming edit batching — rapid
editevents withstreaming: trueare coalesced (~80ms) to reduce React re-renders during agent token streams - SSE / polling fallback when reconnect attempts are exhausted (see transport fallback doc in monorepo)
const conn = client.connectRoom(roomId, {
heartbeatIntervalMs: 25_000,
onOutboundQueueDrop: (n) => console.warn("dropped", n),
});Reconnect backoff defaults — FluxyChat vs Fluxy reference
computeReconnectBackoffMs(attempt, baseMs, maxMs) is min(maxMs, baseMs * 2^attempt), capped internally at attempt ≤ 6 and bounded by maxReconnectAttempts (default 8). The two default profiles produce the schedule below:
| Attempt | FluxyChat default (500ms / 20s) | Fluxy reference (1s / 8s) |
|--------:|--------------------------------------:|--------------------------------:|
| 0 | 500 ms | 1 000 ms |
| 1 | 1 000 ms | 2 000 ms |
| 2 | 2 000 ms | 4 000 ms |
| 3 | 4 000 ms | 8 000 ms (capped) |
| 4 | 8 000 ms | 8 000 ms |
| 5 | 16 000 ms | 8 000 ms |
| 6 | 20 000 ms (capped) | 8 000 ms |
| 7 | 20 000 ms | 8 000 ms |
| Total wall time (8 tries) | ≈ 71.5 s | ≈ 47 s |
Why we keep the wider window (current default)
- Shorter transient drops recover faster. Mobile networks flap in the 1–3 s window; a 500 ms first step lands a reconnect before most apps show a "disconnected" toast, which is what the dashboard UX relies on (
connectionState.statusis rendered in the room header). - Longer outages still give up reasonably fast. The 20 s cap means a 10-minute Worker outage costs ~30 reconnect attempts' worth of traffic, and the SSE/polling fallback (
useChat) kicks in well before users notice. - It's configurable per-room. Call sites that want the Fluxy-style curve set
baseBackoffMs: 1_000, maxBackoffMs: 8_000onconnectRoomoptions; see the P11-B2 entry inROADMAP_EXECUTION.md.
When to flip to the Fluxy profile
- Heavy rooms (≥ 1k concurrent sockets) where each reconnect attempt costs meaningful origin CPU on the Worker, and you'd rather batch retries at 8 s.
- Backend-driven bots / workers where the long backoff is fine because the agent is the only writer.
- Network profiles known to be slow (satellite, intermittent VPNs) where the extra 500 ms either way is in the noise.
When to keep the current default
- Consumer chat UIs, mobile-first apps, anything where the user perceives the first 1–2 s after a drop.
- The dashboard, agent chat, and any room where
presenceInfois shown live.
// Match the Fluxy-style curve (1s base, 8s cap)
const conn = client.connectRoom(roomId, {
baseBackoffMs: 1_000,
maxBackoffMs: 8_000,
});See docs/research/ws-client-benchmark-fluxy.md § 1 for the upstream reference. The defaults are not changing in this minor; P11-B2 stays in the roadmap as a documented decision rather than a code change.
Self-host vs hosted cloud
- Self-host: deploy
apps/workerfrom the monorepo, run D1 migrations, set secrets. Seeapps/worker/.dev.vars.example. - Hosted cloud: use the Fluxychat dashboard on Vercel; sign in with Clerk; project + API keys are provisioned for you.
Full operator docs: docs/dashboard-integration.md.
Agents
Agent invokes run on the Worker (POST /agents/:id/invoke or @handle in a room message). Configure provider keys on the Worker or per-project in the console.
invokeAgentRest(agentId, roomId, content, { replyTo })— REST invoke with optional thread parent message id.getAgentRuns(agentId)— run history includingtool_callswhen tools were used.- Live room events (WebSocket):
tool_call,tool_result,tool_error,agentRun(latency, tokens, status).
History replay (heavy rooms)
const { messages, loadHistory, historyLoaded } = useChat({
roomId,
replay: "request", // skip REST + WS history on connect
});
// later: await loadHistory();Default is replay: "connect". Pass replayHistoryOnReconnect: false to connectRoom if you manage history yourself.
Custom bot streaming (FluxyMessageStream)
For bots outside the built-in agent runtime, stream into one message row over WebSocket:
import { FluxyChatClient, FluxyMessageStream } from "@fluxy-chat/sdk";
const connection = client.connectRoom(roomId);
// connectRoom() auto-connects no .connect() needed
const stream = new FluxyMessageStream(connection, agentUserId, { parentId: null });
stream.push("Hello ");
stream.end();
// Verified against @fluxy-chat/[email protected] 2026-06-26See docs/cookbook/bot-streaming-fluxy-message-stream.md in the monorepo.
P12 / open beta APIs (2026)
| Feature | SDK method |
|---------|------------|
| AI image generation | generateAiImage(roomId, prompt, opts?) — requires AI_IMAGE_GENERATION_ENABLED on Worker |
| Composer tool prompts | buildDeepResearchPrompt(), buildWebSearchPrompt(), buildImageGenerationCaption() |
| AI reply suggestions | suggestReplies(roomId, parentId?) |
| Thread TL;DR | summarizeThread(messageId) |
| Full-text search | searchMessages({ q, roomId?, from?, to? }) |
| Unified inbox | getInbox(), snoozeRoom(), createInboxFollowUp() |
| Room export | exportRoomMarkdown(roomId), exportRoomPdf(roomId) |
| Custom domains (admin) | listCustomDomains(), createCustomDomain(), … |
| Embed config (admin) | getEmbedConfig(), updateEmbedConfig() |
| Quiet hours | getQuietHoursPreferences(), updateQuietHoursPreferences() |
| Digest preferences | getDigestPreferences(), updateDigestPreferences() |
| Agent handoff | getRoomHandoff(), requestRoomHandoff(), resolveRoomHandoff() |
| Agent queue | getAgentQueue(), claimAgentTask(), resolveAgentTask() |
| Feature flags (P12-J) | getFeatureFlags() — backoff applied on room session connect |
| Public host / embed | getPublicHostConfig(), getPublicEmbedConfig() |
Docs in monorepo: docs/README.md · deploy: docs/operations/open-beta-deploy-guide.md
Pusher Channels parity (P9)
FluxyChat maps Pusher Channels features to rooms + JWT. Full matrix: docs/pusher-channels-parity.md.
| Feature | SDK API |
|---------|---------|
| Presence / subscription count | WS events: subscription_succeeded, member_joined, member_left, subscription_count |
| Cache channel | connect(roomId, { cache: true }) |
| E2E encrypted room | getRoomE2eKey(roomId) + encrypted sends |
| User channel | signIn({ userId, roles, ttlSeconds }), connectUser(), triggerUserEvent(), useUserChannel() |
| Multi-room HTTP trigger | triggerEvents({ roomIds, name, data, excludeSocketId }) |
| Exclude sender | excludeSocketId on trigger (no connection.socketId in the SDK) |
| Connection state | connectionState, state_change event |
| Global binding | connection.onAnyEvent() / connection.offAnyEvent(), useChat({ onAnyEvent }) |
| Watchlist | listWatchlist(), addWatchlistTarget(), removeWatchlistTarget() |
| Terminate connections | terminateUserConnections(userId) |
| Public room | Room type: "public" — any project member may connect (JWT still required) |
// signIn() mints a member JWT using the apiKey configured on the client
const signed = await client.signIn({ userId: "alice", roles: ["member"], ttlSeconds: 3600 });
// For React: prefer the `useUserChannel` hook it wraps the WS lifecycle.
// For non-React: use `client.connectUser()` directly (returns a raw WebSocket).
const userWs = client.connectUser(signed.userId);
userWs.onmessage = (event) => console.log("user-channel:", event.data);
await client.triggerEvents({
roomIds: ["lobby", "scores"],
name: "leaderboard",
data: { top: 3 },
});
// onAnyEvent lives on the room connection, not the client
const conn = client.connectRoom("lobby");
conn.onAnyEvent((type, payload) => {
if (type.startsWith("client-")) return;
});
// Verified against @fluxy-chat/[email protected] 2026-06-26import { useUserChannel } from "@fluxy-chat/sdk";
function Inbox({ userId }: { userId: string }) {
const { events, connectionState } = useUserChannel({ userId });
// …
}P10 — Sendbird / Sent / remaining Pusher
See docs/competitive-parity-p10.md.
| Feature | SDK API |
|---------|---------|
| Live channel stats | getRoomLive(roomId) |
| Pin message | pinMessage(roomId, messageId \| null) |
| Terminate one socket | terminateRoomConnection(roomId, socketId) |
| Poll message | createPoll(), votePoll(), getPoll() — WS poll_updated |
| Block user (global) | blockUser(), unblockUser(), listBlocks() |
| Guest open channel | FluxyChatClient.joinPublicRoomAsGuest(baseUrl, roomId) |
| Pusher channel auth | authorizeChannel(socketId, roomIdOrChannel) |
| Message translation | translateMessage(messageId, targetLang, sourceLang?) |
| Delivery receipts | markMessageDelivered(), getMessageDeliveries() — WS delivery_updated |
| FCM push devices | registerPushDevice(), unregisterPushDevice(), listPushDevices() |
| Sent contact sync | syncSentContact(e164, userId?) |
| SMS OTP login | FluxyChatClient.requestSmsOtp() / verifySmsOtp() (API key) |
| Supergroup shards | PATCH /rooms/:id { shardCount: 4 } — see docs/supergroup-room-sharding.md |
const guest = await FluxyChatClient.joinPublicRoomAsGuest(workerUrl, "lobby");
const guestClient = new FluxyChatClient({
baseUrl: workerUrl,
userId: guest.userId,
token: guest.token,
});
const channelAuth = await client.authorizeChannel(socketId, "private-room-lobby");
await client.translateMessage(messageId, "it");
await client.markMessageDelivered(messageId);
await client.registerPushDevice("fcm", fcmToken);Architecture features
FluxyChat includes a full-stack adapter system, streaming markdown renderer, card builder, and AI tool presets — all designed for multi-platform chat deployments.
| Feature | What it does |
|---------|-------------|
| Multi-platform adapters | Pluggable adapter system (Web, WhatsApp, Telegram, Slack, Discord, Teams, Email, SMS, Webhook, Matrix) with unified thread ID encoding and channel mapping |
| Streaming markdown | Real-time markdown rendering with table buffering, code fence tracking, and inline marker healing for partial bold/strikethrough/backtick sequences |
| Card builder | Composable card elements (Text, Button, Image, Divider, Table, Fields) with renderers for fallback text, Markdown, Slack Block Kit, and Teams Adaptive Cards |
| AI tool presets | Pre-configured tool groups (reader, messenger, moderator) with per-tool approval gates for enterprise governance |
| Tool overrides | Customize tool descriptions, titles, and approval requirements per agent profile without duplicating tool code |
| Message format | Canonical mdast AST format for messages, with serialization/deserialization and typed stream chunks (markdown_text, task_update, plan_update) |
| SentMessage factory | Post methods return objects with .edit(), .delete(), .addReaction(), .removeReaction() for cleaner message lifecycle management |
// Multi-platform adapter example
import { getAdapter, dispatchThreadEvent, dispatchMessageEvent } from "@fluxy-chat/sdk";
const adapter = getAdapter("web");
const threadId = adapter.encodeThreadId({ userId, roomId }); // "web:{userId}:{roomId}"
// Streaming markdown
import { StreamingMarkdownRenderer } from "@fluxy-chat/sdk";
const renderer = new StreamingMarkdownRenderer();
renderer.push("**bold** and `code`");
const html = renderer.getHtml(); // renders incrementally
// Card builder
import { Card, Text, Button, cardToMarkdown } from "@fluxy-chat/sdk";
const card = Card(Text("Hello"), Button("Click me", "https://example.com"));
const markdown = cardToMarkdown(card);P22–P25: AI-Native Architecture (2026-07)
Adopted from the Vercel Chat SDK and AI SDK architecture. See ROADMAP_EXECUTION.md for full details.
P22 — Adapter Pattern, Streaming Markdown, Cards, Tool Presets
| Feature | SDK export |
|---------|------------|
| Multi-platform adapters | getAdapter(slug), listAdapters(), BaseAdapter |
| WebAdapter | Registered by default in worker |
| FormatConverter | toAst(text), fromAst(ast), renderPostable(message) |
| StreamingMarkdownRenderer | new StreamingMarkdownRenderer() — .push(), .getHtml(), .finish() |
| Card builder (function API) | Card, Section, Text, Button, LinkButton, Actions, Image, Divider, Field, Fields, Table, CardLink |
| Card builder (JSX) | /** @jsxImportSource @fluxy-chat/sdk */ — custom JSX runtime, no React needed |
| Card rendering | cardToMarkdown(card), cardToFallbackText(card) |
| AI tool presets | getToolPreset("reader" \| "messenger" \| "moderator") |
| Tool overrides | applyToolOverrides(tools, overrides) |
| Concurrency strategies | drop, queue, debounce, burst, concurrent |
| Message format | mdast AST canonical format, StreamChunk union type |
| Serialization | toJSON() / fromJSON() with _type discriminator |
| SentMessage factory | .edit(), .delete(), .addReaction(), .removeReaction() |
| Transcripts API | append(), list(), count(), delete() |
| Custom emoji | createEmoji(), getEmoji() with platform-specific maps |
| Callback URLs | Encode callback tokens in button values for server-side routing |
| Errors | ChatError, RateLimitError, LockError, NotImplementedError |
| Logger | ConsoleLogger with child loggers and log levels |
| Thread state | Per-thread key-value with TTL |
| Mock adapter | MockAdapter for testing |
P23 — AI SDK Core Features
| Feature | SDK export |
|---------|------------|
| Stream resumption | client.resumeStream(streamId), client.getActiveStreams(roomId) |
| Human-in-the-loop approval | needsApproval flag, approval workflow UI cards |
| MCP client | createMcpClient({ transport, url }) — .listTools(), .listResources(), .readResource() |
| MCP tool conversion | convertMcpTools(mcpTools) → FluxyChat tool definitions |
| LLM middleware | wrapLanguageModel(), transformParams(), wrapGenerate(), wrapStream() |
| Middleware composition | composeMiddleware([mw1, mw2, ...]) |
| DevTools web UI | Visual inspector for LLM calls, tool calls, token usage |
| OpenTelemetry | GenAI semantic conventions, @ai-sdk/otel compatible |
| Per-step performance | Model output timing, streaming speed, tool execution time |
| WorkflowAgent | new WorkflowAgent({ agentId, model, tools }) — .step(), .run() |
| Typed runtime context | defineContext(schema) for type-safe cross-step data |
| Sandbox sessions | SandboxSession for isolated code execution |
| Realtime voice | Bidirectional voice-to-voice AI with tool calling |
| Scoped tool context | Per-tool secret/config isolation via contextSchema |
P24 — AI SDK Medium Features
| Feature | SDK export |
|---------|------------|
| Tool call streaming | onInputStart / onInputDelta / onInputAvailable callbacks |
| Multi-step loop control | maxSteps, stopWhen, isStepCount, hasToolCall, isLoopFinished |
| Provider-defined tools | Provider supplies schema, developer provides execute |
| Provider-executed tools | Server-side tools (web search, code execution) |
| Pluggable transport | DefaultChatTransport, ChatTransport interface |
| Typed UIMessage | Generic type parameter for end-to-end type safety |
| Data parts | Stream arbitrary typed data alongside text |
| extractReasoningMiddleware | extractReasoningMiddleware() |
| RAG middleware | createRagMiddleware({ retrieve }) |
| Provider-level middleware | wrapProvider(provider, [middleware]) |
| Image generation | generateImage() API |
| Speech generation | generateSpeech() (TTS) |
| useObject hook | useObject() — stream structured JSON from LLM |
| Structured output | generateObject(), streamObject() with Zod schemas |
| MCP Apps | Sandboxed iframe rendering of tool UIs |
| Slash commands | Cross-platform /command handling |
P25 — AI SDK Low Features
| Feature | SDK export |
|---------|------------|
| experimental_throttle | Configurable render throttle for streaming |
| smoothStream | Flicker-free text streaming |
| Ephemeral messages | User-only visible with DM fallback |
| Cosine similarity | cosineSimilarity(a, b) utility |
| Strict tool calling | strict: true schema enforcement |
| sendAutomaticallyWhen | Auto-submit when all tool results available |
| Sensitive context controls | Prevent secrets in telemetry |
Guides with examples: docs/guides/
Product links
- Hosted quickstart
- Guest demo room
- Compare vs Stream / Ably / Pusher
- Cloudflare Workers chat guide
- Dev.to: build chat on Workers without a socket fleet
Support
Questions: [email protected] · Issues: github.com/AlessandroFare/fluxychat/issues
License
MIT — see LICENSE.
