Skip to main content

Overview

meridian-edge is a Cloudflare Workers runtime for Meridian. It compiles the server to WASM and uses Durable Objects for per-namespace state — one DO instance per namespace, co-located with your users at the edge. The TypeScript SDK connects to it identically to the native server — same WebSocket protocol, same REST API, same token format.
Native serverEdge (Cloudflare Workers)
Runtimetokio + axumCloudflare Workers (WASM)
Storagesled / PostgreSQL / RedisDurable Objects KV
ScalingManual (cluster mode)Automatic (CF global network)
Cold startNone~0ms (DO always warm)
DeployDocker / binarywrangler deploy

Prerequisites

  • Wrangler CLInpm install -g wrangler
  • Rust with wasm32-unknown-unknown target — rustup target add wasm32-unknown-unknown
  • worker-buildcargo install worker-build
  • A Cloudflare account (free tier works)

Local development

cd crates/meridian-edge

# Create your local secrets file (never commit this)
echo 'MERIDIAN_SIGNING_KEY=<your-32-byte-hex-key>' > .dev.vars

# Start local dev server on http://localhost:8787
wrangler dev
Generate a signing key:
openssl rand -hex 32
Generate a token for local testing (uses the same gen_token binary as the native server):
MERIDIAN_SIGNING_KEY=<your-key> cargo run --bin gen_token -- my-namespace 1

Connect with the SDK

import { Effect } from "effect";
import { MeridianClient } from "meridian-sdk";

const client = await Effect.runPromise(
  MeridianClient.create({
    url: "ws://localhost:8787",   // wss://your-worker.workers.dev in prod
    namespace: "my-namespace",
    token: "<generated-token>",
  })
);

const counter = client.gcounter("views");
counter.increment(1);
No SDK changes needed — the edge worker speaks the exact same protocol as the native server.

Production deploy

1. Set your signing key as a secret:
wrangler secret put MERIDIAN_SIGNING_KEY
# Enter your 32-byte hex key when prompted
2. Deploy:
wrangler deploy
Your worker is live at https://meridian-edge.<your-subdomain>.workers.dev. 3. Issue tokens from your backend:
# V1 — glob lists
curl -X POST https://meridian-edge.<your-subdomain>.workers.dev/v1/namespaces/my-room/tokens \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"client_id": 1, "ttl_ms": 3600000, "permissions": {"read": ["*"], "write": ["*"]}}'

# V2 — fine-grained rules with op masks and {clientId} scoping
curl -X POST https://meridian-edge.<your-subdomain>.workers.dev/v1/namespaces/my-room/tokens \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{"client_id": 42, "ttl_ms": 3600000, "rules": {"r": [{"p": "*"}], "w": [{"p": "or:cart-{clientId}"}]}}'

# Inspect your token claims
curl https://meridian-edge.<your-subdomain>.workers.dev/v1/namespaces/my-room/tokens/me \
  -H "Authorization: Bearer <your-token>"
See Tokens for the full V2 field reference.

Architecture

Each namespace maps to one Durable Object instance:
Client SDK
    │  WebSocket / REST

CF Worker (WASM)          ← meridian-edge: auth, routing
    │  DO RPC

NsObject (Durable Object) ← one per namespace: CRDT state, WS fan-out
    │  KV storage

Durable Object Storage    ← persistent, replicated by Cloudflare
When a client connects:
  1. Worker validates the token (ed25519 signature, namespace check)
  2. Worker looks up the DO for the namespace via env.NS_OBJECT.idFromName(namespace)
  3. WebSocket is upgraded and forwarded to the DO
  4. The DO manages all connected WebSockets and broadcasts deltas on every op

Webhooks

Register a URL to be called on every op applied to a namespace. Useful for triggering backend pipelines, notifications, or audit systems without maintaining a persistent WebSocket.
# Register a webhook (admin token required)
curl -X POST https://meridian-edge.<your-subdomain>.workers.dev/v1/namespaces/my-room/webhooks \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-backend.com/meridian-hook",
    "secret": "my-secret",
    "crdt_ids": ["visits", "messages"]
  }'
Each op fires a POST to the registered URL with this payload:
{
  "crdt_id": "visits",
  "seq": 42,
  "timestamp_ms": 1700000000000
}
The X-Meridian-Secret header is included if a secret was registered. Use it to verify the request origin.
# List webhooks
curl https://meridian-edge.<your-subdomain>.workers.dev/v1/namespaces/my-room/webhooks \
  -H "Authorization: Bearer <admin-token>"

# Delete a webhook
curl -X DELETE https://meridian-edge.<your-subdomain>.workers.dev/v1/namespaces/my-room/webhooks/<id> \
  -H "Authorization: Bearer <admin-token>"
Webhook delivery is best-effort — failed requests are not retried. For guaranteed delivery, use the WAL endpoint to replay missed ops.

WAL & point-in-time recovery

Every op written to a Durable Object is persisted to a write-ahead log before being applied to the CRDT snapshot. This means you can replay ops from any sequence number:
# Fetch WAL entries from seq 0 (admin token required)
curl https://meridian-edge.<your-subdomain>.workers.dev/v1/namespaces/my-room/wal?from_seq=0 \
  -H "Authorization: Bearer <admin-token>"
Response:
{
  "checkpoint_seq": 42,
  "entries": [
    { "seq": 43, "crdt_id": "views", "op_bytes": "...", "timestamp_ms": 1700000000000 }
  ]
}
WAL entries older than 7 days are automatically compacted via DO Alarms — no manual maintenance required. Namespaces inactive for 30 days are automatically deleted.

wrangler.toml reference

name = "meridian-edge"
main = "build/index.js"
compatibility_date = "2024-01-01"

[build]
command = "cargo install -q worker-build && worker-build --release"

[[durable_objects.bindings]]
name = "NS_OBJECT"
class_name = "NsObject"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["NsObject"]

Limitations

  • Single DO instance per namespace — no cross-region replication at the application level. Cloudflare handles availability automatically; for strict data residency use DO jurisdictions.
  • No Prometheus endpoint — Workers have no long-running process to scrape. Use Cloudflare Analytics Engine or Workers Logpush for equivalent observability.
  • Query Engine performancePOST /v1/namespaces/:ns/query is fully supported but scans all crdt:* keys in DO KV on every request. For namespaces with thousands of CRDTs, prefer the native server which can use indexed storage backends.
  • Live query overheadSubscribeQuery subscriptions are persisted to DO KV (one write per subscription). Each op re-evaluates all matching live queries by scanning DO KV. This is acceptable for typical namespaces; for very high-frequency ops with many live subscribers, the native server’s in-memory registry is more efficient.