npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ws-kit/cloudflare

v0.10.0

Published

Cloudflare Durable Objects adapter for WS-Kit enabling stateful, globally-distributed WebSocket applications at the edge

Readme

@ws-kit/cloudflare

Cloudflare Durable Objects platform adapter for WS-Kit with per-instance pub/sub and explicit multi-DO federation.

Purpose

@ws-kit/cloudflare provides the platform-specific integration layer for WS-Kit on Cloudflare Durable Objects, enabling:

  • Per-instance WebSocket broadcasting via BroadcastChannel
  • State management integration with durable storage
  • Explicit cross-DO federation via RPC for multi-shard coordination
  • Type-safe handler composition with core router
  • Zero-copy message broadcasting within a DO instance

What This Package Provides

  • createDurableObjectHandler(): Factory returning fetch handler for DO integration
  • DurablePubSub: BroadcastChannel-based pub/sub for per-instance messaging
  • federate() helpers: Explicit multi-DO coordination functions
  • Sharding helpers: topicToDoName(), getShardedDoId(), getShardedStub() for stable topic-to-shard routing
  • UUID v7 client IDs: Time-ordered unique identifiers per connection
  • Resource tracking: Automatic resourceId and connectedAt metadata
  • Connection limits: Per-DO instance connection quota enforcement

Platform Advantages Leveraged

  • Per-Instance Isolation: Each DO instance is isolated; broadcasts don't cross shards automatically
  • Durable Storage: Direct access to persistent key-value storage via DurableObjectState
  • BroadcastChannel: Low-latency in-memory messaging to all connections within a DO
  • Cost Optimization: Broadcasts are free; only pays for fetch calls
  • Automatic Failover: Cloudflare automatically restarts failed DO instances
  • Strong Isolation: Per-resource instances prevent cross-tenant leaks

Installation

bun add @ws-kit/core @ws-kit/cloudflare

Install with a validator adapter (optional but recommended):

bun add zod @ws-kit/zod
# OR
bun add valibot @ws-kit/valibot

Dependencies

  • @ws-kit/core (required) — Core router and types
  • uuid (required) — For UUID v7 client ID generation
  • @cloudflare/workers-types (peer) — TypeScript types for Cloudflare Workers (only in TypeScript projects)

Quick Start

Create a Durable Object handler for your WebSocket router:

import { z, message, createRouter } from "@ws-kit/zod";
import { createDurableObjectHandler } from "@ws-kit/cloudflare";

type AppData = { userId?: string };

const PingMessage = message("PING", { text: z.string() });
const PongMessage = message("PONG", { reply: z.string() });

const router = createRouter<AppData>();

router.on(PingMessage, (ctx) => {
  ctx.send(PongMessage, { reply: `Got: ${ctx.payload.text}` });
});

const handler = createDurableObjectHandler(router);

export default {
  fetch(req: Request, state: DurableObjectState, env: Env) {
    return handler.fetch(req);
  },
};

Advanced Configuration

For custom authentication or options, you can pass them to the handler factory:

import { createDurableObjectHandler } from "@ws-kit/cloudflare";
import { createRouter } from "@ws-kit/zod";

const router = createRouter();

const handler = createDurableObjectHandler(router, {
  authenticate: async (req) => {
    const token = req.headers.get("authorization");
    // Verify token and return user data
    return { userId: "user_123" };
  },
  maxConnections: 500, // Optional: limit connections per DO
});

export default {
  fetch(req: Request, state: DurableObjectState, env: Env) {
    return handler.fetch(req);
  },
};

⚠️ Critical: Per-Instance Broadcast Scope

In Cloudflare DO, router.publish() broadcasts ONLY to WebSocket connections within THIS DO instance, not across shards.

For multi-DO setups, use the federate() helper for explicit cross-DO coordination:

import { federate } from "@ws-kit/cloudflare";

router.on(AnnouncementSchema, async (ctx) => {
  const rooms = ["room:1", "room:2", "room:3"];

  // Explicitly broadcast to multiple shards
  await federate(env.ROOMS, rooms, async (room) => {
    await room.fetch(
      new Request("https://internal/announce", {
        method: "POST",
        body: JSON.stringify({ text: ctx.payload.text }),
      }),
    );
  });
});

This design is intentional: each DO is isolated for clarity and cost control.

API Reference

createDurableObjectHandler(options)

Returns a fetch handler compatible with Durable Object script.

Options:

  • router: WebSocketRouter — Router instance to handle messages
  • authenticate?: (req: Request) => Promise<TData | undefined> | TData | undefined — Custom auth function
  • context?: unknown — Custom context passed to handlers
  • maxConnections?: number — Maximum concurrent connections (default: 1000)

Connection Data

All connections automatically include:

type DurableObjectWebSocketData<T> = {
  clientId: string; // UUID v7 - unique per connection
  resourceId?: string; // Extracted from URL (?room=... or path)
  connectedAt: number; // Timestamp in milliseconds
  // + your custom auth data (T)
};

federate() Helpers

federate(namespace, shardIds, action) - Basic federation

await federate(env.ROOMS, ["room:1", "room:2"], async (room) => {
  await room.fetch(
    new Request("https://internal/announce", {
      method: "POST",
      body: JSON.stringify({ event: "ANNOUNCEMENT" }),
    }),
  );
});

federateWithErrors(namespace, shardIds, action) - With error details

const results = await federateWithErrors(env.ROOMS, roomIds, async (room) => {
  return await room.fetch(new Request("https://internal/sync"));
});

federateWithFilter(namespace, shardIds, filter, action) - Conditional federation

// Only notify US regions
await federateWithFilter(
  env.ROOMS,
  allRoomIds,
  (id) => id.startsWith("us:"),
  async (room) => {
    await room.fetch("https://internal/us-announcement");
  },
);

Sharding Helpers

When using Cloudflare Durable Objects with pub/sub, each DO instance is limited to 100 concurrent connections. Use sharding helpers to distribute subscriptions across multiple DO instances by computing a stable shard from the scope name.

topicToDoName(topic, shards, prefix) - Compute shard name from topic

import { topicToDoName } from "@ws-kit/cloudflare";

// Same topic always routes to same shard
topicToDoName("room:general", 10); // → "ws-router-2"
topicToDoName("room:general", 10); // → "ws-router-2" (consistent)
topicToDoName("room:random", 10); // → "ws-router-7"

getShardedDoId(env, topic, shards, prefix) - Get DO ID for a topic

import { getShardedDoId } from "@ws-kit/cloudflare";

const doId = getShardedDoId(env, `room:${roomId}`, 10);
const stub = env.ROUTER.get(doId);

getShardedStub(env, topic, shards, prefix) - Get DO stub ready for fetch

import { getShardedStub } from "@ws-kit/cloudflare";

export default {
  async fetch(req: Request, env: Env) {
    const roomId = new URL(req.url).searchParams.get("room") ?? "general";
    const stub = getShardedStub(env, `room:${roomId}`, 10);
    return stub.fetch(req); // Routes to sharded DO
  },
};

Benefits:

  • Linear scaling: Add more DO instances to handle more concurrent connections
  • Stable routing: Same topic always routes to same DO instance
  • No cross-shard coordination: Each topic's subscribers live on one DO
  • Deterministic: Same shard map every time (no crypto, stable hash)

Important: Changing the shard count will remap existing topics. Plan accordingly and consider a migration period if using persistent storage.

Examples

Chat Application

import { z, message, createRouter } from "@ws-kit/zod";

const JoinRoom = message("JOIN_ROOM", { room: z.string() });
const SendMessage = message("SEND_MESSAGE", { text: z.string() });
const RoomList = message("ROOM:LIST", { users: z.array(z.string()) });
const RoomMessage = message("ROOM:MESSAGE", {
  user: z.string(),
  text: z.string(),
});

type AppData = { clientId?: string; room?: string };
const router = createRouter<AppData>();

const members = new Set<string>();

router.on(JoinRoom, async (ctx) => {
  const { room } = ctx.payload;
  const { clientId } = ctx.data;

  members.add(clientId!);
  ctx.assignData({ room });
  await ctx.topics.subscribe(`room:${room}`);

  // Broadcast updated member list using schema
  await router.publish(`room:${room}`, RoomList, {
    users: Array.from(members),
  });
});

router.on(SendMessage, async (ctx) => {
  const room = ctx.data.room || "general";
  const { clientId } = ctx.data;

  // Broadcast message using schema
  await router.publish(`room:${room}`, RoomMessage, {
    user: clientId!,
    text: ctx.payload.text,
  });
});

Game Server with State

const GameStateMessage = message("GAME:STATE", {
  action: z.string(),
  playerId: z.string(),
});

router.on(GameActionSchema, async (ctx) => {
  // Save state
  await state.storage.put(`action:${Date.now()}`, ctx.payload);

  // Broadcast to all players in this game
  await router.publish("game:state", GameStateMessage, {
    action: ctx.payload.action,
    playerId: ctx.payload.playerId,
  });
});

TypeScript Support

Full TypeScript support with generic TData type parameter for custom connection data and type inference from message schemas.

Related Packages

License

MIT