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
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.
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.
- pusher ↗
A **Pusher-protocol-compatible** WebSocket broadcaster
- workers ↗
Cloudflare Workers + Durable Objects
- durable-objects ↗
"new_sqlite_classes": ["Beacon"]
- architecture ↗
The Worker entry routes both WS upgrades (`/app/{APP_KEY}`) and the REST API (`/apps/{APP_ID}/...`) to the same beacon.
- free-tier-eligible ↗
100,000 per day
- free-tier-eligible ↗
SQLite
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
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 ↗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 ↗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
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.
- 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
- 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
- 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.
deploy: wrangler deploy
Repository README
View original on GitHub ↗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.
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)
- Click the button above. Cloudflare forks this repo into your GitHub and sets up a Worker with the default
APP_IDandAPP_KEY. - 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 - (Recommended) Change
APP_KEYinwrangler.jsoncfromgondor-calls-for-aidto something non-obvious, thennpx 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:
- Point your backend's Pusher SDK at
https://<your-worker-host>(signs REST calls withAPP_SECRET). - Point your browser/app Pusher client at
wss://<your-worker-host>/app/<APP_KEY>. - 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 (
pusherpackage) — PRs welcome - Ruby on Rails (
pushergem) — 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:
- Client connects and is assigned a
socket_id. - Client attempts to subscribe to a private or presence channel.
- The Pusher client POSTs
{ socket_id, channel_name }to your backend's signing endpoint. - Your backend's auth logic decides whether this user is allowed on this channel.
- If allowed, the backend signs
HMAC_SHA256("{socket_id}:{channel}", APP_SECRET)and returns the token. - Client sends
pusher:subscribewith 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.

Discussion · 0
sign in to comment →