Cloudsteading
Product image still needed. This listing has source documentation, but no reviewed screenshot yet.

The WebSocket Sower

Realtime WebSocket channels with presence, built on a single Worker + one Durable Object class.

The WebSocket Sower is a self-hosted Pusher Channels alternative built on Cloudflare (Durable Objects, WebSockets, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

No recognized license found

License TL;DR

We haven’t established reuse permission. Public code on GitHub doesn’t by itself give you permission to copy, change or sell it. Read the actual terms or ask the maintainer before reusing the code.

Understand missing or unfamiliar licenses →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗

Repository owner

@cloudsteading

See the upstream repository for the original creator and contributors.

Listing managed by flindev.

Cloudflare hosting

Free tier eligible within limits

Workers and SQLite Durable Objects have Free-plan allowances, conditional on request, CPU, socket/message and storage usage. Hosting has not been metered. The repository has no established license, so source reuse permission remains unresolved.

Hosting requirements
  • Inspect and resolve the missing license before reusing or redistributing this repository.
  • Use SQLite Durable Objects as declared in the migration; KV-backed objects have different plan requirements.
  • Keep account-wide dynamic requests within 100,000/day and Worker CPU within 10ms per invocation; separately respect Durable Object daily request, duration and storage allowances.
  • Set a real APP_SECRET and your own app identifiers; the documented one-click deployment link still contains a placeholder owner.
  • A fresh deployment, security audit, commercial protocol parity and billing have not been verified.
Check current pricing ↗
Review findings & limitations
  • License missing: README asks the maintainer to add a license; no license file was collected. This pre-existing listing is not included in the new freely licensed publication batch.
  • Deployment button still references YOUR_USERNAME/gondor-ws; configure your own repository and credentials.
  • Source review only; no fresh deployment or measured total hosting cost.
Sources checked 01/10/2026

Repository snapshot: 094a8fa. Hosting eligibility reflects the deployment documentation and listed assumptions.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

Pusher Channels logoPusher Channels ↗

Pusher Channels protocol v7 broadcasting with public/private/presence subscriptions and signed REST events; encrypted channels, webhooks and multi-tenant support are absent. Full compatibility has not been tested.

See supporting source ↗
Workflow overlap
Pricing not verified

How it works

The shape of The WebSocket Sower on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points1
gondor-ws
wrangler.jsonc
↓
App
gondor-ws
entry
Cloudflare Workers
Entrypoint: src/index.ts
↓

Configuration and workflow sources

Reviewed commit 094a8fa13a01. This configuration evidence comes from reading source files. Execution checks, when available, appear in the project's runtime review.

Deployment configuration · 1 file
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2026-04-07

gondor-ws · default

Entrypoint: src/index.ts

  • BEACON → Durable Objects · class Beacon

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

src/index.ts ↗
  • L8 · fetch handler exported · references APP_SECRET, APP_KEY, APP_ID · calls isSecretConfigured, path.startsWith, path.slice, forwardToBeacon
  • L43 · forwardToBeacon calls (conditional paths may differ): env.BEACON.idFromName, fetch, env.BEACON.get

Environment references: env.APP_SECRET · env.APP_KEY · env.APP_ID · env.BEACON

src/beacon.ts ↗

    Environment references: env.APP_ID · env.APP_KEY · env.APP_SECRET

    src/auth.ts ↗
    • L12 · hmacSha256Hex calls (conditional paths may differ): crypto.subtle.importKey, enc.encode, crypto.subtle.sign, join, map, padStart, b.toString
    • L25 · timingSafeEqStr calls (conditional paths may differ): Buffer.from, nodeTimingSafeEqual
    • L32 · bodyMd5 calls (conditional paths may differ): digest, update, createHash
    • L42 · verifyChannelAuth calls (conditional paths may differ): auth.indexOf, auth.slice, timingSafeEqStr, hmacSha256Hex
    • L74 · verifyRestSignature calls (conditional paths may differ): query.get, Number, Number.isFinite, Math.floor, Date.now, Math.abs, timingSafeEqStr, bodyMd5, pairs.push, k.toLowerCase, pairs.sort, join, pairs.map, method.toUpperCase, hmacSha256Hex
    src/protocol.ts ↗
    • L17 · isValidChannelName calls (conditional paths may differ): CHANNEL_NAME_RE.test
    • L21 · frame calls (conditional paths may differ): JSON.stringify
    • L30 · errorFrame calls (conditional paths may differ): frame
    • L34 · parseFrame calls (conditional paths may differ): JSON.parse
    • L57 · isPrivateChannel calls (conditional paths may differ): name.startsWith
    • L61 · isPresenceChannel calls (conditional paths may differ): name.startsWith
    • L65 · isClientEvent calls (conditional paths may differ): event.startsWith
    • L70 · generateSocketId calls (conditional paths may differ): crypto.getRandomValues
    Build and deployment pipeline · 0 GitHub Actions workflows

    Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

    No GitHub Actions workflow was found in the collected tree. Deployment may be manual or configured elsewhere.

    package.json ↗
    • deploy: wrangler deploy
    ChannelsClient eventsPresencePrivate channelsWebSockets

    Full upstream document by @cloudsteading · README.md · snapshot 094a8fa

    gondor-ws

    "The beacons! The beacons of Minas Tirith! The beacons are lit — Gondor calls for aid!"

    A Pusher-protocol-compatible WebSocket broadcaster that runs on Cloudflare Workers + Durable Objects. One does not simply pay Pusher fees.

    Because it speaks the Pusher Channels protocol v7, every existing Pusher client — pusher-js in the browser, pusher-php-server on the backend, the Swift/Android/Flutter SDKs, Laravel Echo, anything that connects to pusher.com today — works against this server with only a hostname change.

    Idle beacons hibernate (zero billing while unlit), there are no egress fees, and a typical small-to-medium app runs on the Cloudflare free tier or for $5/month flat.

    ⚠️ Update the repo URL below to point at your fork before publishing.

    Deploy to Cloudflare


    The lore

    In the films, each beacon on the White Mountains holds a single purpose: watch, and when lit, relay to the next. That's exactly what a Cloudflare Durable Object does here. One Beacon per app holds every subscriber, stays dark (hibernates) until an event arrives, then fans the event out to every watcher.

    What you get

    • Pusher protocol v7 — full handshake, subscribe/unsubscribe, ping/pong, error frames
    • Public / private / presence channels with HMAC-SHA256 signature verification
    • Client events (client-*) relayed between subscribers on private/presence channels
    • Server REST API — /events, /batch_events, /channels, /channels/{ch}, /channels/{ch}/users — fully signed and replay-protected
    • Hibernation-safe — per-socket state in serializeAttachment, presence roster in SQLite, nothing pins the isolate. Unlit beacons stop billing duration.
    • Rate limited + size capped — 10 client events/sec/connection, 10 KiB payloads, 100 channels/socket

    Light the beacon (one-click deploy)

    1. Click the button above. Cloudflare forks this repo into your GitHub and sets up a Worker with the default APP_ID and APP_KEY.
    2. Set the shared secret — this is what lets the receiver trust the signal is real:
      # In your local clone:
      npx wrangler secret put APP_SECRET
      # Paste a 32+ char random value. Generate with: openssl rand -hex 32
      
    3. (Recommended) Change APP_KEY in wrangler.jsonc from gondor-calls-for-aid to something non-obvious, then npx wrangler deploy.

    Until step 2 is done the server returns 503 APP_SECRET is unset — by design. A freshly-forked beacon will not light without a real secret.

    Local development

    npm install
    cp .dev.vars.example .dev.vars   # put a real dev secret inside
    npm run dev                      # wrangler dev on http://127.0.0.1:8787
    

    Framework integrations

    Because gondor-ws speaks the Pusher wire protocol, it works with any Pusher-compatible client. The common path is:

    1. Point your backend's Pusher SDK at https://<your-worker-host> (signs REST calls with APP_SECRET).
    2. Point your browser/app Pusher client at wss://<your-worker-host>/app/<APP_KEY>.
    3. Your backend signs subscription auth tokens for private/presence channels using the same APP_SECRET; the Worker verifies them.
    Setup guides
    • Laravel (Echo + pusher-js + Broadcasting)
    • Node.js (pusher package) — PRs welcome
    • Ruby on Rails (pusher gem) — PRs welcome
    • Python (pusher-http-python) — PRs welcome
    • Go (pusher-http-go) — PRs welcome

    If you've wired it up to a stack not listed here, please contribute a guide.

    Generic configuration reference

    You'll use exactly the same three values in both your backend and frontend:

    Env var What it is Where it goes
    APP_ID Public identifier for REST URLs (/apps/{APP_ID}/events) Backend Pusher SDK config
    APP_KEY Public key sent on WS handshake (wss://host/app/{APP_KEY}) Frontend + backend
    APP_SECRET Secret HMAC key, ≥ 16 chars Backend only. Never ships to the client.

    And point the Pusher client at the Worker:

    host:   <your-subdomain>.workers.dev   (or your custom domain)
    port:   443
    scheme: https
    useTLS: true
    

    How session authorization works

    The handshake is deliberately public — it only validates APP_KEY. Per-user authorization happens at channel subscribe time, exactly like Pusher does it:

    1. Client connects and is assigned a socket_id.
    2. Client attempts to subscribe to a private or presence channel.
    3. The Pusher client POSTs { socket_id, channel_name } to your backend's signing endpoint.
    4. Your backend's auth logic decides whether this user is allowed on this channel.
    5. If allowed, the backend signs HMAC_SHA256("{socket_id}:{channel}", APP_SECRET) and returns the token.
    6. Client sends pusher:subscribe with the token to this Worker, which verifies the signature.

    The APP_SECRET never leaves your backend or the Worker. The socket_id binding prevents token replay across connections — one signed token lights one beacon on one socket, not the whole chain.

    Architecture

    One Beacon per app. Holds every WebSocket for that app, maintains channel subscription indexes in memory, persists the presence roster and per-socket metadata so it can rebuild after hibernation. The Worker entry routes both WS upgrades (/app/{APP_KEY}) and the REST API (/apps/{APP_ID}/...) to the same beacon.

    Scaling past one beacon

    Single-beacon ceiling is ~5,000 concurrent sockets or ~1,000 subscribers on a single broadcast. If you need more, light more beacons: hash the channel name to one of N beacons, route both WS upgrades (by the first channel a socket subscribes to) and REST triggers (by channel) to the matching shard. Nothing crosses shards.

    Hibernation rules this repo follows
    • No setInterval / setTimeout / outgoing WebSockets / alarms
    • Per-socket state is in serializeAttachment (≤ 2 KB)
    • Presence user_info (potentially large) is in SQLite, not the attachment
    • In-memory Maps are rebuilt in the constructor from getWebSockets() + SQL

    Unlit beacons evict from memory and stop billing duration. Ping/pong is handled by the runtime and does not wake a hibernating beacon.

    Security

    Threat Mitigation
    Unauthorized channel access Backend's signing endpoint + signed subscribe
    Auth token replay across connections Signature binds to socket_id
    Signed REST request replay Rejects auth_timestamp older than 10 min
    Client event DoS / amplification 10 events/sec/conn, 10 KiB/event caps
    Large REST trigger amplification 10 KiB payload cap, 10 events/batch cap
    Channel name pollution Pusher-regex validation, 164 char max
    Unset secret in fresh deploy Server returns 503 until a real APP_SECRET is configured
    Predictable socket IDs weakening binding IDs generated via crypto.getRandomValues()

    What's not implemented

    • Encrypted channels (private-encrypted-*)
    • Webhooks back to your app (channel_occupied, member_added, etc.)
    • Cache channels (last-message replay)
    • Per-user terminate-connections endpoint
    • Multi-tenant (multiple APP_KEYs on one Worker) — deliberately single-tenant

    None of these block everyday real-time use. PRs welcome. And Rohan will answer.

    License

    Add your license of choice here (MIT, Apache-2.0, etc.).

    Frequently asked about The WebSocket Sower

    What is The WebSocket Sower?+

    The WebSocket Sower is a self-hosted Pusher Channels alternative built on the Cloudflare developer platform. Realtime WebSocket channels with presence, built on a single Worker + one Durable Object class.

    What does The WebSocket Sower replace?+

    The WebSocket Sower is listed as an alternative to Pusher Channels. Compare the features and tradeoffs before migrating.

    What Cloudflare primitives does The WebSocket Sower use?+

    The WebSocket Sower is built on Durable Objects, WebSockets, Workers.

    How much does The WebSocket Sower cost to run?+

    Workers and SQLite Durable Objects have Free-plan allowances, conditional on request, CPU, socket/message and storage usage. Hosting has not been metered. The repository has no established license, so source reuse permission remains unresolved. Inspect and resolve the missing license before reusing or redistributing this repository. Use SQLite Durable Objects as declared in the migration; KV-backed objects have different plan requirements. Keep account-wide dynamic requests within 100,000/day and Worker CPU within 10ms per invocation; separately respect Durable Object daily request, duration and storage allowances. Set a real APP_SECRET and your own app identifiers; the documented one-click deployment link still contains a placeholder owner. A fresh deployment, security audit, commercial protocol parity and billing have not been verified. Check current Cloudflare pricing before deploying.

    Is The WebSocket Sower open source?+

    A source repository is linked at https://github.com/cloudsteading/websocket-sower, but this catalog has not established its license. Check the repository license before using or redistributing the code.

    Community rating

    No ratings yet. Tried this project? Share your experience.

    One rating per verified account. You can change or remove yours. Accounts are email verified; use of the software is self-reported.

    Discuss your experience ↓
    Sign in to rate

    Discussion · 0

    sign in to comment →
    No comments yet — be the first.