UNPKG

@durable-streams/y-durable-streams

Version:

Yjs provider for Durable Streams - sync Yjs documents over append-only streams

263 lines (206 loc) 7.32 kB
--- name: yjs-server description: > Deploy Yjs collaborative editing. YjsServer setup with compaction threshold, Caddy reverse proxy with flush_interval -1 for SSE, 3-layer architecture (Browser → Caddy → YjsServer → DS Server), Electric Cloud managed alternative with @electric-sql/cli provisioning. Load when deploying y-durable-streams to production or configuring server infrastructure. type: core library: durable-streams library_version: "0.2.3" requires: - yjs-getting-started sources: - "durable-streams/durable-streams:packages/y-durable-streams/src/server/yjs-server.ts" - "durable-streams/durable-streams:packages/y-durable-streams/src/server/compaction.ts" - "durable-streams/durable-streams:examples/yjs-demo/server.ts" - "durable-streams/durable-streams:examples/yjs-demo/Caddyfile" --- This skill builds on durable-streams/yjs-getting-started. Read it first for basic setup. # Durable Streams — Yjs Server Deployment Three deployment options: dev server for prototyping, Caddy for self-hosted production, Electric Cloud for managed hosting. ## Architecture ``` Browser (YjsProvider) │ HTTPS ▼ Caddy reverse proxy (:443) ├─ /v1/stream/* → Durable Streams storage └─ /v1/yjs/* → YjsServer (flush_interval -1) │ HTTP ▼ DS Server (storage) ``` YjsServer implements the Yjs wire protocol (snapshot discovery, compaction, awareness routing) and proxies all storage operations to a Durable Streams server. ## Development ```typescript import { DurableStreamTestServer } from "@durable-streams/server" import { YjsServer } from "@durable-streams/y-durable-streams/server" const dsServer = new DurableStreamTestServer({ port: 4437 }) await dsServer.start() const yjsServer = new YjsServer({ port: 4438, host: "127.0.0.1", dsServerUrl: "http://localhost:4437", compactionThreshold: 1024 * 1024, // 1MB (default) }) await yjsServer.start() ``` ### YjsServer options | Option | Default | Description | | --------------------- | --------------- | --------------------------------------------------- | | `port` | — | Listen port | | `host` | `"127.0.0.1"` | Listen host | | `dsServerUrl` | — | Backing DS server URL | | `compactionThreshold` | `1048576` (1MB) | Trigger compaction after this many bytes of updates | | `dsServerHeaders` | `{}` | Headers sent to the DS server (e.g. auth) | ### Compaction When accumulated updates for a document exceed `compactionThreshold`, the server automatically creates a snapshot. New clients load the snapshot instead of replaying all updates — keeps initial sync fast. Connected clients are unaffected. ## Production with Caddy Download the Caddy binary with the durable_streams plugin from [GitHub releases](https://github.com/durable-streams/durable-streams/releases). ### Caddyfile ``` :443 { route /v1/stream/* { durable_streams { data_dir ./data max_file_handles 200 } } route /v1/yjs/* { reverse_proxy localhost:4438 { flush_interval -1 } } } ``` **`flush_interval -1` is mandatory** — without it, Caddy buffers SSE responses and live updates stop working. This is the #1 production deployment mistake. ### Production YjsServer Point YjsServer at the Caddy server (not the raw DS server) if Caddy handles TLS: ```typescript const yjsServer = new YjsServer({ port: 4438, dsServerUrl: "https://localhost:443", compactionThreshold: 1024 * 1024, }) ``` ## Managed with Electric Cloud Skip infrastructure setup entirely. Provision a Yjs service via the Electric Cloud CLI: ```bash # Install and authenticate npx @electric-sql/cli auth login # Create a Yjs service npx @electric-sql/cli services create yjs --json # Get the service URL and secret npx @electric-sql/cli services get-secret <service-id> --json ``` Then point YjsProvider at the cloud URL: ```typescript const provider = new YjsProvider({ doc, baseUrl: "https://api.electric-sql.cloud/v1/yjs/<service-id>", docId: "my-doc", headers: { Authorization: `Bearer <secret>`, }, }) ``` ### Server-side proxy (required for browser apps) Do NOT expose the Electric Cloud secret to browser clients. Use a server-side proxy route that injects the Authorization header: ```typescript // Server route: /api/yjs/* app.all("/api/yjs/*", async (req, res) => { const targetUrl = `https://api.electric-sql.cloud/v1/yjs/<service-id>${req.path.replace("/api/yjs", "")}` const response = await fetch(targetUrl, { method: req.method, headers: { ...req.headers, Authorization: `Bearer ${process.env.YJS_SECRET}`, }, body: req.method !== "GET" ? req.body : undefined, duplex: "half", }) // Block-list headers that break when proxied const skipHeaders = new Set([ "content-encoding", "content-length", "transfer-encoding", "connection", ]) for (const [key, value] of response.headers) { if (!skipHeaders.has(key.toLowerCase())) { res.setHeader(key, value) } } res.status(response.status) response.body.pipe(res) }) ``` Key proxy rules: - Use a **block-list** for response headers — Yjs protocol uses custom headers like `stream-next-offset` that an allow-list would miss - Block `content-encoding` and `content-length` — Node's `fetch` auto-decompresses gzip but leaves the headers, causing `ERR_CONTENT_DECODING_FAILED` - Use `duplex: "half"` when forwarding request bodies Then point the provider at your proxy: ```typescript const provider = new YjsProvider({ doc, baseUrl: "/api/yjs", // Must be absolute — use window.location.origin + path docId: "my-doc", }) ``` ## Common Mistakes ### CRITICAL Missing `flush_interval -1` in Caddy config Wrong: ``` route /v1/yjs/* { reverse_proxy localhost:4438 } ``` Correct: ``` route /v1/yjs/* { reverse_proxy localhost:4438 { flush_interval -1 } } ``` Without this, Caddy buffers SSE responses. Live updates appear to hang — clients connect but never receive data. Source: examples/yjs-demo/Caddyfile ### HIGH Exposing Electric Cloud secret to browser clients Wrong: ```typescript new YjsProvider({ doc, baseUrl: "https://api.electric-sql.cloud/v1/yjs/<service-id>", headers: { Authorization: `Bearer ${cloudSecret}` }, // Leaked! }) ``` Correct: Use a server-side proxy that injects the secret. See the proxy section above. ### MEDIUM Not configuring compaction threshold Default is 1MB. For documents with frequent small edits (collaborative text), this is reasonable. For documents with large binary content (images, files), increase it to avoid excessive compaction I/O. ## See also - [yjs-getting-started](../yjs-getting-started/SKILL.md) — Dev server setup - [yjs-sync](../yjs-sync/SKILL.md) — Provider configuration and events - [server-deployment](../../../client/skills/server-deployment/SKILL.md) — DS server Caddy config - [go-to-production](../../../client/skills/go-to-production/SKILL.md) — HTTPS, TTL, CDN checklist