> ## Documentation Index
> Fetch the complete documentation index at: https://chahine.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Edge deploy (Cloudflare Workers)

> Deploy Meridian to Cloudflare Workers with Durable Objects — zero infrastructure, global low latency.

## 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 server | Edge (Cloudflare Workers) |
| - | - | - |
| Runtime | tokio + axum | Cloudflare Workers (WASM) |
| Storage | sled / PostgreSQL / Redis | Durable Objects KV |
| Scaling | Manual (cluster mode) | Automatic (CF global network) |
| Cold start | None | \~0ms (DO always warm) |
| Deploy | Docker / binary | `wrangler deploy` |

## Prerequisites

* [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) — `npm install -g wrangler`
* Rust with `wasm32-unknown-unknown` target — `rustup target add wasm32-unknown-unknown`
* `worker-build` — `cargo install worker-build`
* A Cloudflare account (free tier works)

## Local development

```bash theme={null}
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:

```bash theme={null}
openssl rand -hex 32
```

Generate a token for local testing (uses the same `gen_token` binary as the native server):

```bash theme={null}
MERIDIAN_SIGNING_KEY=<your-key> cargo run --bin gen_token -- my-namespace 1
```

## Connect with the SDK

```ts theme={null}
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:**

```bash theme={null}
wrangler secret put MERIDIAN_SIGNING_KEY
# Enter your 32-byte hex key when prompted
```

**2. Deploy:**

```bash theme={null}
wrangler deploy
```

Your worker is live at `https://meridian-edge.<your-subdomain>.workers.dev`.

**3. Issue tokens from your backend:**

```bash theme={null}
# 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](https://chahine.mintlify.site/self-hosting/tokens.md) 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.

```bash theme={null}
# 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:

```json theme={null}
{
  "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.

```bash theme={null}
# 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:

```bash theme={null}
# 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:

```json theme={null}
{
  "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](https://developers.cloudflare.com/durable-objects/api/alarms/) — no manual maintenance required. Namespaces inactive for **30 days** are automatically deleted.

## wrangler.toml reference

```toml theme={null}
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](https://developers.cloudflare.com/durable-objects/reference/data-location/).
* **No Prometheus endpoint** — Workers have no long-running process to scrape. Use [Cloudflare Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/) or [Workers Logpush](https://developers.cloudflare.com/workers/observability/logpush/) for equivalent observability.
* **Query Engine performance** — `POST /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 overhead** — `SubscribeQuery` 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.