Cloudflare backend

pulsify-cloud-server/cloudflare runs Pulsify Cloud on Cloudflare Workers. A stateless Worker authenticates every request; one Durable Object per root holds the root's documents, grants and sockets in its own SQLite database.

ClientWorkerObject aliceObject bobupgrade /v1/roots/aliceverify tokenidFromName(alice)101, hibernatable socketframes of alice/*upgrade /v1/roots/bobidFromName(bob)frames of bob/*
The Worker never stores anything; each root's Durable Object owns its data.

Why Durable Objects

  • Automatic scaling. Each root is its own object, so capacity grows with the number of roots. There is no shared database to size.
  • Global distribution. An object is created near the first request for its root, which is usually its owner.
  • Pay as you go. Sockets use the hibernation API: an idle root is evicted from memory while its sockets stay open, and wakes up on the next frame. Heartbeat pings are answered by the runtime (setWebSocketAutoResponse) without waking it.
  • Serialized writes. An object processes one event at a time, so appends, acknowledgements and broadcasts of a root are ordered without locks.

1. Install

terminal
bun add pulsify-core pulsify-cloud pulsify-cloud-server
bun add -d wrangler

2. Write the Worker

Two exports: the Durable Object class and the fetch handler.

worker.ts
// worker.ts
export { PulsifyCloudRoot, cloudWorker as default } from "pulsify-cloud-server/cloudflare";

Or mount the cloud routes next to your own:

worker.ts
// worker.ts, when the Worker also serves other routes
import { handleCloudRequest, type PulsifyCloudEnv } from "pulsify-cloud-server/cloudflare";
export { PulsifyCloudRoot } from "pulsify-cloud-server/cloudflare";

export default {
  async fetch(request: Request, env: PulsifyCloudEnv) {
    const { pathname } = new URL(request.url);
    if (pathname.startsWith("/v1/")) return handleCloudRequest(request, env);
    return new Response("Hello");
  },
};

3. Configure Wrangler

The binding must be named PULSIFY_ROOTS and the class must use SQLite storage (new_sqlite_classes).

wrangler.jsonc
// wrangler.jsonc
{
  "name": "pulsify-cloud",
  "main": "worker.ts",
  "compatibility_date": "2026-11-05",
  "durable_objects": {
    "bindings": [{ "name": "PULSIFY_ROOTS", "class_name": "PulsifyCloudRoot" }]
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["PulsifyCloudRoot"] }]
  // Optional data residency for every root:
  // "vars": { "PULSIFY_JURISDICTION": "eu" }
}

4. Set the secrets and deploy

terminal
openssl rand -base64 48 | bunx wrangler secret put PULSIFY_TOKEN_SECRET
openssl rand -base64 48 | bunx wrangler secret put PULSIFY_API_KEY
bunx wrangler deploy
# https://pulsify-cloud.<account>.workers.dev  →  your PULSIFY_URL

Use the same PULSIFY_TOKEN_SECRET in the backend that signs tokens. Keep PULSIFY_API_KEY on your servers. Then follow the tutorial with the Worker URL.

Local development

terminal
# .dev.vars (never commit)
PULSIFY_TOKEN_SECRET="a local secret of at least thirty-two characters"
PULSIFY_API_KEY="a local api key of at least thirty-two characters"

bunx wrangler dev   # http://localhost:8787, Durable Objects and SQLite included
terminal
curl http://localhost:8787/v1/health
# {"ok":true}
curl -X PUT http://localhost:8787/v1/roots/alice/grants \
  -H "Authorization: Bearer $PULSIFY_API_KEY" \
  -d '{"subject":"alice","prefix":"alice","role":"owner"}'

What runs where

PieceRole
WorkerRoutes /v1/roots/{root}/…, verifies the token or API key, negotiates the envelope, strips the credential and forwards to the root's object.
PulsifyCloudRootWraps CloudRoot from pulsify-cloud-server over the object's SQLite: patch rows, the grant table and the subscription table that survives hibernation.
Socket attachmentConnection ID, subject and envelope, restored after wake-up.

PULSIFY_JURISDICTION places every root in a jurisdiction such as eu. Set it before the first deployment: the jurisdiction is part of the object ID, so changing it later points every root to a new, empty object.

Limits and costs

  • A root is one object: one thread and one SQLite database, with the per-object storage limit of your Cloudflare plan. Split very large or very busy namespaces into several roots.
  • Cloudflare bills Durable Object requests, duration and storage. Every WebSocket message is a request; hibernated sockets add no duration.
  • Frames are limited to 8 MiB by Pulsify; uploads are chunked well below that.
terminal
bun run test:cloud --backend cloudflare   # this repository: workerd + conformance suite