Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.
Y-S2 is a Cloudflare Worker that serves Yjs documents over WebSockets, using S2 as the durable, ordered log of each room’s updates and an R2 bucket for snapshots. Any number of Worker instances can serve the same room: they coordinate only through the room’s S2 stream, so there is no central server to run or scale.
Rooms — each room is a stream named rooms/<room>/index in your basin, created on first use.
Writes — document updates and presence (cursor) updates from a client are batched and appended to the room’s stream. Messages larger than S2’s 1 MiB record limit, such as a large paste, are stored in R2 and the stream carries a pointer to them.
Fan-out — every connection reads the room’s stream and relays new records to its client.
Joining — a new connection loads the room’s latest snapshot from R2, reads the stream from where that snapshot ends, and sends the client the full document once it has caught up.
Snapshots — while a room is active, connections periodically write a new snapshot to R2 and then trim the stream up to it. A lease built on fencing tokens ensures only one connection snapshots a room at a time.
A Cloudflare account on the Workers Paid plan. Each WebSocket connection is a single Worker invocation, and every S2 and R2 call it makes counts toward the per-invocation subrequest limit, which is 50 on the free plan.
An R2 bucket for snapshots and large messages.
An S2 basin with Create stream on append and Create stream on read enabled, and a scoped access token for it.
Node.js 22 or newer.
Set the basin’s default stream retention to infinite, or at least longer than a room may go without edits. Snapshots are only taken while a room is active, so updates that expire from the stream before a snapshot covers them are lost. Infinite retention requires a payment method on your S2 account (see limits).
Point the R2_BUCKET binding in wrangler.jsonc at your bucket by setting its bucket_name.
Store your S2 credentials as Worker secrets:
npx wrangler secret put S2_ACCESS_TOKENnpx wrangler secret put S2_BASIN
Deploy:
npm run deploy
For local development, put S2_ACCESS_TOKEN and S2_BASIN in a .dev.vars file and run npm run dev. The repository also includes a GitHub Actions workflow that checks and deploys every push to main; see its README for the secrets it expects.
These optional variables can be set under vars in wrangler.jsonc (or in .dev.vars locally):
Variable
Default
Description
LOG_MODE
CONSOLE
CONSOLE logs to the Worker console. S2_SINGLE logs each connection to its own stream (logs/worker-<cf-ray>), and S2_SHARED logs all connections to logs/workers-shared, in the same basin. Case-insensitive.
SNAPSHOT_BACKLOG_SIZE
400
Number of records since the last snapshot that triggers a new one.
SNAPSHOT_BACKLOG_BYTES
8388608
Size in bytes of the records since the last snapshot that triggers a new one.
BACKLOG_BUFFER_AGE
60000
Milliseconds after the oldest record not yet in a snapshot before a new one is taken.
LEASE_DURATION
30
Seconds a connection holds the snapshot lease before others may take it over.
S2_BATCH_SIZE
8
Maximum number of records per append batch.
S2_LINGER_TIME
50
Milliseconds to wait for more records before sending an append batch.
Connect with the y-websocket provider. The server URL ends in /ws, and the room is passed both as the room name and as the room parameter:
import * as Y from 'yjs'import { WebsocketProvider } from 'y-websocket'const room = 'room-name'const doc = new Y.Doc()const provider = new WebsocketProvider( 'wss://your-worker.your-subdomain.workers.dev/ws', room, doc, { params: { room, yauth: 'your-auth-token' } })
Presence works through the provider’s awareness, as with any y-websocket server.
Y-S2 does not authenticate clients yet: any non-empty yauth value is accepted, and /auth/token returns a fixed demo token. Add your own authorization before using it for anything but demos. Anyone who can reach the Worker can read and write any room, and create new rooms in your basin.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"S2","description":"API for unlimited, durable, real-time streams.","colors":{"primary":"#252525","light":"#E8E8E8","dark":"#252525"},"logo":{"light":"https://mintcdn.com/streamstore/2KVWF55FbBFAHG9D/logo/s2-black.png?fit=max\u0026auto=format\u0026n=2KVWF55FbBFAHG9D\u0026q=85\u0026s=a21a60dc6c4cc961bb234a8eba956217","dark":"https://mintcdn.com/streamstore/2KVWF55FbBFAHG9D/logo/s2-white.png?fit=max\u0026auto=format\u0026n=2KVWF55FbBFAHG9D\u0026q=85\u0026s=7cd77fc75c133fddce332121b1d71f64","href":"https://s2.dev/"},"favicon":"https://mintcdn.com/streamstore/2KVWF55FbBFAHG9D/logo/s2-black.png?fit=max\u0026auto=format\u0026n=2KVWF55FbBFAHG9D\u0026q=85\u0026s=a21a60dc6c4cc961bb234a8eba956217","api":{"openapi":"specs-denorm/v1/openapi.json","examples":{"defaults":"required","languages":["curl"]}},"appearance":{"default":"system","strict":false},"background":{"color":{"light":"#FFFFFF","dark":"#252525"}},"navbar":{"links":[{"href":"https://github.com/s2-streamstore/s2","label":"s2-streamstore/s2","type":"github"},{"href":"https://discord.gg/vTCs7kMkAf","label":"s2.dev","type":"discord"}]},"footer":{"socials":{"bluesky":"https://bsky.app/profile/s2.dev","x":"https://x.com/s2_streamstore","linkedin":"https://www.linkedin.com/company/streamstore","github":"https://github.com/s2-streamstore","discord":"https://discord.gg/vTCs7kMkAf"}},"seo":{"metatags":{"canonical":"https://s2.dev/docs"}},"integrations":{"posthog":{"apiKey":"phc_o0QA5I7imsTAAtxVm57BogVdpN0U7e70SmwD5lzaiAJ","apiHost":"https://php.s2.dev"}},"contextual":{"options":["copy","view","chatgpt","claude"],"display":"header"},"styling":{"latex":false},"navigation":{"global":{"anchors":[{"anchor":"Blog","icon":"newspaper","href":"https://s2.dev/blog"},{"anchor":"Talk to us","icon":"comments","href":"https://s2.dev/contact"}]},"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"streamstore","hasAudioNarration":false,"actualSubdomain":"streamstore","customDomains":["s2.dev"],"gitSource":{"type":"github","owner":"s2-streamstore","repo":"docs","deployBranch":"main","contentDirectory":"","isPrivate":true,"mountPath":"","label":""},"chroma":{"liveDeploymentHistoryId":"6adbbb7a6d07b13661cd85a0","collectionId":"146c9708-f08f-4ebd-9e49-a0d476507bca","schemaVersion":"v3","latestBuiltDeploymentHistoryId":"6adbbb7a6d07b13661cd85a0"},"feedback":{"thumbs":false},"entitlements":{"AI_CHAT":{"status":"ENABLED"},"WEB_MCP":{"status":"ENABLED"},"NARRATION":{"status":"ENABLED"}},"buildId":"6adbbb7a6d07b13661cd85a0","clientVersion":"0.0.4006","searchFilterCounts":[{"language":"default","tag":"API","count":27},{"language":"default","tag":"CLI","count":15},{"language":"default","tag":"Data Plane","count":15},{"language":"default","tag":"SDKs","count":14},{"language":"default","tag":"Concepts","count":13},{"language":"default","tag":"Integrations","count":13},{"language":"default","tag":"Platform","count":10},{"language":"default","tag":"Updates","count":10},{"language":"default","tag":"Control Plane","count":9},{"language":"default","tag":"Start","count":8}]},"assistantConfig":{},"basePath":"/docs","hasChatPermissions":true,"hasWebMcpPermissions":true,"toggles":[],"googleFontsInlined":false},"customCss":[{"filePath":"custom.css","href":"/docs/_mintlify/custom/streamstore/c2f785041ee9d423/custom.css"},{"filePath":"asciinema-player.css","content":"@keyframes ap-loader-rotation{0%{transform:rotate(0deg)}to{transform:rotate(360deg)}}.ap-default-term-ff{--term-font-family:\"Cascadia Code\", \"Source Code Pro\", Menlo, Consolas, \"DejaVu Sans Mono\", monospace, \"Symbols Nerd Font\"}div.ap-wrapper{outline:0;height:100%;display:flex;justify-content:center}div.ap-wrapper .title-bar{display:none;top:-78px;transition:top .15s linear;position:absolute;left:0;right:0;box-sizing:content-box;font-size:20px;line-height:1em;padding:15px;font-family:sans-serif;color:#fff;background-color:rgba(0,0,0,.8)}div.ap-wrapper .title-bar img{vertical-align:middle;height:48px;margin-right:16px}div.ap-wrapper .title-bar a{color:#fff;text-decoration:underline}div.ap-wrapper .title-bar a:hover{text-decoration:none}div.ap-wrapper:fullscreen{background-color:#000;width:100%;align-items:center}div.ap-wrapper:fullscreen .title-bar{display:initial}div.ap-wrapper:fullscreen.hud .title-bar{top:0}div.ap-wrapper div.ap-player{text-align:left;display:inline-block;padding:0;position:relative;box-sizing:content-box;overflow:hidden;max-width:100%;border-radius:4px;font-size:15px;background-color:var(--term-color-background)}.ap-player{--term-color-foreground:#ffffff;--term-color-background:#000000;--term-color-0:var(--term-color-foreground);--term-color-1:var(--term-color-foreground);--term-color-2:var(--term-color-foreground);--term-color-3:var(--term-color-foreground);--term-color-4:var(--term-color-foreground);--term-color-5:var(--term-color-foreground);--term-color-6:var(--term-color-foreground);--term-color-7:var(--term-color-foreground);--term-color-8:var(--term-color-0);--term-color-9:var(--term-color-1);--term-color-10:var(--term-color-2);--term-color-11:var(--term-color-3);--term-color-12:var(--term-color-4);--term-color-13:var(--term-color-5);--term-color-14:var(--term-color-6);--term-color-15:var(--term-color-7)}div.ap-term{position:relative;font-family:var(--term-font-family);border-width:.75em;border-radius:0;border-style:solid;border-color:var(--term-color-background);box-sizing:content-box}div.ap-term canvas,div.ap-term svg.ap-term-symbols{position:absolute;inset:0;display:block;width:100%;height:100%}div.ap-term svg.ap-term-symbols{overflow:hidden;pointer-events:none}.ap-line span,div.ap-term svg.ap-term-symbols use{color:var(--term-color-foreground)}div.ap-term svg.ap-term-symbols:not(.ap-blink) .ap-blink{opacity:0}div.ap-term pre.ap-term-text{position:absolute;inset:0;box-sizing:content-box;overflow:hidden;padding:0;margin:0;display:block;white-space:pre;word-wrap:normal;word-break:normal;cursor:text;color:var(--term-color-foreground);outline:0;line-height:var(--term-line-height);font-family:inherit;font-size:inherit;font-variant-ligatures:none;border:0;border-radius:0;background-color:transparent!important}pre.ap-term-text .ap-line{display:block;width:100%;height:var(--term-line-height);position:absolute;top:calc(100%*var(--row)/var(--term-rows));letter-spacing:normal;overflow:hidden}pre.ap-term-text .ap-line span{position:absolute;left:calc(100%*var(--offset)/var(--term-cols));padding:0;display:inline-block;height:100%}pre.ap-term-text:not(.ap-blink) .ap-line .ap-blink{color:transparent;border-color:transparent}pre.ap-term-text .ap-bold{font-weight:700}pre.ap-term-text .ap-faint{opacity:.5}pre.ap-term-text .ap-underline{text-decoration:underline}pre.ap-term-text .ap-italic{font-style:italic}pre.ap-term-text .ap-strike{text-decoration:line-through}div.ap-player div.ap-control-bar{width:100%;height:32px;display:flex;justify-content:space-between;align-items:stretch;color:var(--term-color-foreground);box-sizing:content-box;line-height:1;position:absolute;bottom:0;left:0;opacity:0;transition:opacity .15s linear;user-select:none;border-top:2px solid color-mix(in oklab,var(--term-color-background) 80%,var(--term-color-foreground));z-index:30}div.ap-player div.ap-control-bar *{box-sizing:inherit}div.ap-control-bar svg.ap-icon path{fill:var(--term-color-foreground)}div.ap-control-bar span.ap-button{display:flex;flex:0 0 auto;cursor:pointer}div.ap-control-bar span.ap-playback-button{width:12px;height:12px;padding:10px;margin:0 0 0 2px}div.ap-control-bar span.ap-playback-button svg{height:12px;width:12px}div.ap-control-bar span.ap-timer{display:flex;flex:0 0 auto;min-width:50px;margin:0 10px;height:100%;text-align:center;font-size:13px;line-height:100%;cursor:default}div.ap-control-bar span.ap-timer span{font-family:var(--term-font-family);font-size:inherit;font-weight:600;margin:auto}div.ap-control-bar span.ap-timer .ap-time-remaining,div.ap-control-bar span.ap-timer:hover .ap-time-elapsed{display:none}div.ap-control-bar span.ap-timer:hover .ap-time-remaining{display:flex}div.ap-control-bar .ap-progressbar{display:block;flex:1 1 auto;height:100%;padding:0 10px}div.ap-control-bar .ap-progressbar .ap-bar{display:block;position:relative;cursor:default;height:100%;font-size:0}div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter{display:block;position:absolute;top:15px;left:0;right:0;height:3px}div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-empty{background-color:color-mix(in oklab,var(--term-color-foreground) 20%,var(--term-color-background))}div.ap-control-bar .ap-progressbar .ap-bar .ap-gutter-full{width:100%;transform-origin:left center;background-color:var(--term-color-foreground);border-radius:3px}div.ap-control-bar.ap-seekable .ap-progressbar .ap-bar{cursor:pointer}div.ap-control-bar .ap-fullscreen-button{width:14px;height:14px;padding:9px;margin:0 2px 0 4px}div.ap-control-bar .ap-fullscreen-button svg{width:14px;height:14px}div.ap-control-bar .ap-fullscreen-button svg.ap-icon-fullscreen-on{display:inline}div.ap-control-bar .ap-fullscreen-button svg.ap-icon-fullscreen-off{display:none}div.ap-control-bar .ap-fullscreen-button .ap-tooltip,div.ap-control-bar .ap-kbd-button .ap-tooltip{right:5px;left:initial;transform:none}div.ap-control-bar .ap-kbd-button{height:14px;padding:9px;margin:0 0 0 4px}div.ap-control-bar .ap-kbd-button svg{width:26px;height:14px}div.ap-control-bar .ap-speaker-button{width:19px;padding:6px 9px;margin:0 0 0 4px;position:relative}div.ap-control-bar .ap-speaker-button svg{width:19px}div.ap-control-bar .ap-speaker-button .ap-tooltip{left:-50%;transform:none}div.ap-wrapper.ap-hud .ap-control-bar{opacity:1}div.ap-wrapper:fullscreen .ap-fullscreen-button svg.ap-icon-fullscreen-on{display:none}div.ap-wrapper:fullscreen .ap-fullscreen-button svg.ap-icon-fullscreen-off{display:inline}span.ap-progressbar span.ap-marker-container{display:block;top:0;bottom:0;width:21px;position:absolute;margin-left:-10px}span.ap-marker-container span.ap-marker{display:block;top:13px;bottom:12px;left:7px;right:7px;background-color:color-mix(in oklab,var(--term-color-foreground) 33%,var(--term-color-background));position:absolute;transition:top .1s,bottom .1s,left .1s,right .1s,background-color .1s;border-radius:50%}span.ap-marker-container span.ap-marker.ap-marker-past{background-color:var(--term-color-foreground)}span.ap-marker-container span.ap-marker:hover,span.ap-marker-container:hover span.ap-marker{background-color:var(--term-color-foreground);top:11px;bottom:10px;left:5px;right:5px}.ap-tooltip-container span.ap-tooltip{visibility:hidden;font-family:var(--term-font-family);font-weight:700;text-align:center;padding:0 .5em;border-radius:4px;position:absolute;z-index:1;white-space:nowrap;font-size:13px;line-height:2em;bottom:100%;left:50%;transform:translateX(-50%)}.ap-tooltip-container:hover span.ap-tooltip{visibility:visible}.ap-player .ap-overlay{z-index:10;background-repeat:no-repeat;background-position:center;position:absolute;top:0;left:0;right:0;bottom:0;display:flex;justify-content:center;align-items:center}.ap-player .ap-overlay-start{cursor:pointer}.ap-player .ap-overlay-start .ap-play-button{font-size:0;position:absolute;left:0;top:0;right:0;bottom:0;text-align:center;color:#fff;height:80px;max-height:66%;margin:auto}.ap-player .ap-overlay-start .ap-play-button div{height:100%}.ap-player .ap-overlay-start .ap-play-button div span{height:100%;display:block}.ap-player .ap-overlay-start .ap-play-button div span svg{height:100%;display:inline-block}.ap-player .ap-overlay-start .ap-play-button svg{filter:drop-shadow(0 0 5px rgba(0,0,0,.4))}.ap-player .ap-overlay-loading .ap-loader{width:48px;height:48px;border-radius:50%;display:inline-block;position:relative;border:10px solid;border-color:rgba(255,255,255,.3) rgba(255,255,255,.5) rgba(255,255,255,.7) #fff;border-color:color-mix(in srgb,var(--term-color-foreground) 30%,var(--term-color-background)) color-mix(in srgb,var(--term-color-foreground) 50%,var(--term-color-background)) color-mix(in srgb,var(--term-color-foreground) 70%,var(--term-color-background)) color-mix(in srgb,var(--term-color-foreground) 100%,var(--term-color-background));box-sizing:border-box;animation:ap-loader-rotation 1s linear infinite}.ap-player .ap-overlay-help>div div,.ap-player .ap-overlay-info{background-color:var(--term-color-background)}.ap-player .ap-overlay-help>div div kbd,.ap-player .ap-overlay-info span,.ap-tooltip-container span.ap-tooltip{color:var(--term-color-background);background-color:var(--term-color-foreground)}.ap-player .ap-overlay-info span{font-family:var(--term-font-family);font-size:2em;font-weight:700;padding:.5em .75em;text-transform:uppercase}.ap-player .ap-overlay-help{background-color:rgba(0,0,0,.8);container-type:inline-size}.ap-player .ap-overlay-help>div{font-family:var(--term-font-family);max-width:85%;max-height:85%;font-size:18px;color:var(--term-color-foreground);box-sizing:border-box;margin-bottom:32px}.ap-player .ap-overlay-help>div div{padding:calc(min(4cqw,40px));font-size:calc(min(1.9cqw,18px));border:1px solid color-mix(in oklab,var(--term-color-background) 90%,var(--term-color-foreground));border-radius:6px}.ap-player .ap-overlay-help>div div p{font-weight:700;margin:0 0 2em}.ap-player .ap-overlay-help>div div ul{list-style:none;padding:0}.ap-player .ap-overlay-help>div div ul li{margin:0 0 .75em}.ap-player .ap-overlay-help>div div kbd{padding:.2em .5em;border-radius:.2em;font-family:inherit;font-size:.85em;border:0;margin:0}.ap-player .ap-overlay-error span{font-size:8em}.ap-player .slide-enter-active{transition:opacity .2s}.ap-player .slide-enter-active.ap-was-playing{transition:top .2s ease-out,opacity .2s}.ap-player .slide-exit-active{transition:top .2s ease-in,opacity .2s}.ap-player .slide-enter{top:-50%;opacity:0}.ap-player .slide-enter-to{top:0}.ap-player .slide-enter,.ap-player .slide-enter-to,.ap-player .slide-exit,.ap-player .slide-exit-to{bottom:auto;height:100%}.ap-player .slide-exit{top:0}.ap-player .slide-exit-to{top:-50%;opacity:0}.asciinema-player-theme-asciinema{--term-color-foreground:#cccccc;--term-color-background:#121314;--term-color-0:#000000;--term-color-1:#dd3c69;--term-color-2:#4ebf22;--term-color-3:#ddaf3c;--term-color-4:#26b0d7;--term-color-5:#b954e1;--term-color-6:#54e1b9;--term-color-7:#d9d9d9;--term-color-8:#4d4d4d;--term-color-9:#dd3c69;--term-color-10:#4ebf22;--term-color-11:#ddaf3c;--term-color-12:#26b0d7;--term-color-13:#b954e1;--term-color-14:#54e1b9;--term-color-15:#ffffff}.asciinema-player-theme-dracula{--term-color-foreground:#f8f8f2;--term-color-background:#282a36;--term-color-0:#21222c;--term-color-1:#ff5555;--term-color-2:#50fa7b;--term-color-3:#f1fa8c;--term-color-4:#bd93f9;--term-color-5:#ff79c6;--term-color-6:#8be9fd;--term-color-7:#f8f8f2;--term-color-8:#6272a4;--term-color-9:#ff6e6e;--term-color-10:#69ff94;--term-color-11:#ffffa5;--term-color-12:#d6acff;--term-color-13:#ff92df;--term-color-14:#a4ffff;--term-color-15:#ffffff}.asciinema-player-theme-monokai{--term-color-foreground:#f8f8f2;--term-color-background:#272822;--term-color-0:#272822;--term-color-1:#f92672;--term-color-2:#a6e22e;--term-color-3:#f4bf75;--term-color-4:#66d9ef;--term-color-5:#ae81ff;--term-color-6:#a1efe4;--term-color-7:#f8f8f2;--term-color-8:#75715e;--term-color-15:#f9f8f5}.asciinema-player-theme-nord{--term-color-foreground:#eceff4;--term-color-background:#2e3440;--term-color-0:#3b4252;--term-color-1:#bf616a;--term-color-2:#a3be8c;--term-color-3:#ebcb8b;--term-color-4:#81a1c1;--term-color-5:#b48ead;--term-color-6:#88c0d0;--term-color-7:#eceff4}.asciinema-player-theme-seti{--term-color-foreground:#cacecd;--term-color-background:#111213;--term-color-0:#323232;--term-color-1:#c22832;--term-color-2:#8ec43d;--term-color-3:#e0c64f;--term-color-4:#43a5d5;--term-color-5:#8b57b5;--term-color-6:#8ec43d;--term-color-7:#eeeeee;--term-color-15:#ffffff}.asciinema-player-theme-solarized-dark,.asciinema-player-theme-solarized-light{--term-color-0:#073642;--term-color-1:#dc322f;--term-color-2:#859900;--term-color-3:#b58900;--term-color-4:#268bd2;--term-color-5:#d33682;--term-color-6:#2aa198;--term-color-7:#eee8d5;--term-color-8:#002b36;--term-color-9:#cb4b16;--term-color-10:#586e75;--term-color-12:#839496;--term-color-13:#6c71c4;--term-color-14:#93a1a1;--term-color-15:#fdf6e3}.asciinema-player-theme-solarized-dark{--term-color-foreground:#839496;--term-color-background:#002b36;--term-color-11:#657b83}.asciinema-player-theme-solarized-light{--term-color-foreground:#657b83;--term-color-background:#fdf6e3;--term-color-11:#657c83}.asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-fill{fill:var(--term-color-1)}.asciinema-player-theme-solarized-light .ap-overlay-start .ap-play-button svg .ap-play-btn-stroke{stroke:var(--term-color-1)}.asciinema-player-theme-tango{--term-color-foreground:#cccccc;--term-color-background:#121314;--term-color-0:#000000;--term-color-1:#cc0000;--term-color-2:#4e9a06;--term-color-3:#c4a000;--term-color-4:#3465a4;--term-color-5:#75507b;--term-color-6:#06989a;--term-color-7:#d3d7cf;--term-color-8:#555753;--term-color-9:#ef2929;--term-color-10:#8ae234;--term-color-11:#fce94f;--term-color-12:#729fcf;--term-color-13:#ad7fa8;--term-color-14:#34e2e2;--term-color-15:#eeeeec}.asciinema-player-theme-gruvbox-dark{--term-color-foreground:#fbf1c7;--term-color-background:#282828;--term-color-0:#282828;--term-color-1:#cc241d;--term-color-2:#98971a;--term-color-3:#d79921;--term-color-4:#458588;--term-color-5:#b16286;--term-color-6:#689d6a;--term-color-7:#a89984;--term-color-8:#7c6f65;--term-color-9:#fb4934;--term-color-10:#b8bb26;--term-color-11:#fabd2f;--term-color-12:#83a598;--term-color-13:#d3869b;--term-color-14:#8ec07c;--term-color-15:#fbf1c7}"}],"page":{"pagePath":"integrations/y-s2","slug":"/integrations/y-s2","pageMetadata":{"title":"Y-S2","sidebarTitle":"Yjs","description":"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.","href":"/integrations/y-s2"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,li:`li`,ol:`ol`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,Heading,Table,Warning}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsxs(_components.p,{children:[_jsx(_components.a,{href:`https://github.com/s2-streamstore/y-s2`,children:`Y-S2`}),` is a Cloudflare Worker that serves `,_jsx(_components.a,{href:`https://yjs.dev`,children:`Yjs`}),` documents over WebSockets, using S2 as the durable, ordered log of each room’s updates and an R2 bucket for snapshots. Any number of Worker instances can serve the same room: they coordinate only through the room’s S2 stream, so there is no central server to run or scale.`]}),`\n`,_jsx(Heading,{level:`2`,id:`how-it-works`,children:`How it works`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Rooms`}),` — each room is a stream named `,_jsx(_components.code,{children:`rooms/\u003croom>/index`}),` in your basin, created on first use.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Writes`}),` — document updates and presence (cursor) updates from a client are batched and appended to the room’s stream. Messages larger than S2’s 1 MiB `,_jsx(_components.a,{href:`/platform/limits#records`,children:`record limit`}),`, such as a large paste, are stored in R2 and the stream carries a pointer to them.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Fan-out`}),` — every connection reads the room’s stream and relays new records to its client.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Joining`}),` — a new connection loads the room’s latest snapshot from R2, reads the stream from where that snapshot ends, and sends the client the full document once it has caught up.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Snapshots`}),` — while a room is active, connections periodically write a new snapshot to R2 and then `,_jsx(_components.a,{href:`/concepts/trimming`,children:`trim`}),` the stream up to it. A lease built on `,_jsx(_components.a,{href:`/concepts/concurrency-control`,children:`fencing tokens`}),` ensures only one connection snapshots a room at a time.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`See `,_jsx(_components.a,{href:`https://s2.dev/blog/durable-yjs-rooms`,children:`Behind y-s2: serverless multiplayer rooms`}),` for a deeper walkthrough.`]}),`\n`,_jsx(Heading,{level:`2`,id:`prerequisites`,children:`Prerequisites`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[`A Cloudflare account on the `,_jsx(_components.strong,{children:`Workers Paid`}),` plan. Each WebSocket connection is a single Worker invocation, and every S2 and R2 call it makes counts toward the per-invocation subrequest limit, which is 50 on the free plan.`]}),`\n`,_jsx(_components.li,{children:`An R2 bucket for snapshots and large messages.`}),`\n`,_jsxs(_components.li,{children:[`An S2 basin with `,_jsx(_components.strong,{children:`Create stream on append`}),` and `,_jsx(_components.strong,{children:`Create stream on read`}),` enabled, and a scoped `,_jsx(_components.a,{href:`/concepts/access-tokens`,children:`access token`}),` for it.`]}),`\n`,_jsx(_components.li,{children:`Node.js 22 or newer.`}),`\n`]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[`Set the basin’s default stream retention to `,_jsx(_components.strong,{children:`infinite`}),`, or at least longer than a room may go without edits. Snapshots are only taken while a room is active, so updates that expire from the stream before a snapshot covers them are lost. Infinite retention requires a payment method on your S2 account (see `,_jsx(_components.a,{href:`/platform/limits#retention`,children:`limits`}),`).`]})}),`\n`,_jsx(Heading,{level:`2`,id:`deploy`,children:`Deploy`}),`\n`,_jsxs(_components.ol,{children:[`\n`,_jsxs(_components.li,{children:[`\n`,_jsx(_components.p,{children:`Clone the repository and install dependencies:`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`3`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`3`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`git`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` clone`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` https://github.com/s2-streamstore/y-s2`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:`cd`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` y-s2`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`npm`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` install`})]}),`\n`]})})}),`\n`]}),`\n`,_jsxs(_components.li,{children:[`\n`,_jsxs(_components.p,{children:[`Point the `,_jsx(_components.code,{children:`R2_BUCKET`}),` binding in `,_jsx(_components.code,{children:`wrangler.jsonc`}),` at your bucket by setting its `,_jsx(_components.code,{children:`bucket_name`}),`.`]}),`\n`]}),`\n`,_jsxs(_components.li,{children:[`\n`,_jsx(_components.p,{children:`Store your S2 credentials as Worker secrets:`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`2`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`2`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`npx`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` wrangler`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` secret`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` put`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` S2_ACCESS_TOKEN`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`npx`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` wrangler`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` secret`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` put`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` S2_BASIN`})]}),`\n`]})})}),`\n`]}),`\n`,_jsxs(_components.li,{children:[`\n`,_jsx(_components.p,{children:`Deploy:`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`npm`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` run`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` deploy`})]}),`\n`]})})}),`\n`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`For local development, put `,_jsx(_components.code,{children:`S2_ACCESS_TOKEN`}),` and `,_jsx(_components.code,{children:`S2_BASIN`}),` in a `,_jsx(_components.code,{children:`.dev.vars`}),` file and run `,_jsx(_components.code,{children:`npm run dev`}),`. The repository also includes a GitHub Actions workflow that checks and deploys every push to `,_jsx(_components.code,{children:`main`}),`; see its `,_jsx(_components.a,{href:`https://github.com/s2-streamstore/y-s2#continuous-deployment`,children:`README`}),` for the secrets it expects.`]}),`\n`,_jsx(Heading,{level:`2`,id:`configuration`,children:`Configuration`}),`\n`,_jsxs(_components.p,{children:[`These optional variables can be set under `,_jsx(_components.code,{children:`vars`}),` in `,_jsx(_components.code,{children:`wrangler.jsonc`}),` (or in `,_jsx(_components.code,{children:`.dev.vars`}),` locally):`]}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Variable`}),_jsx(_components.th,{children:`Default`}),_jsx(_components.th,{children:`Description`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`LOG_MODE`})}),_jsx(_components.td,{children:_jsx(_components.code,{children:`CONSOLE`})}),_jsxs(_components.td,{children:[_jsx(_components.code,{children:`CONSOLE`}),` logs to the Worker console. `,_jsx(_components.code,{children:`S2_SINGLE`}),` logs each connection to its own stream (`,_jsx(_components.code,{children:`logs/worker-\u003ccf-ray>`}),`), and `,_jsx(_components.code,{children:`S2_SHARED`}),` logs all connections to `,_jsx(_components.code,{children:`logs/workers-shared`}),`, in the same basin. Case-insensitive.`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`SNAPSHOT_BACKLOG_SIZE`})}),_jsx(_components.td,{\"data-numeric\":`true`,children:_jsx(_components.code,{children:`400`})}),_jsx(_components.td,{children:`Number of records since the last snapshot that triggers a new one.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`SNAPSHOT_BACKLOG_BYTES`})}),_jsx(_components.td,{\"data-numeric\":`true`,children:_jsx(_components.code,{children:`8388608`})}),_jsx(_components.td,{children:`Size in bytes of the records since the last snapshot that triggers a new one.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`BACKLOG_BUFFER_AGE`})}),_jsx(_components.td,{\"data-numeric\":`true`,children:_jsx(_components.code,{children:`60000`})}),_jsx(_components.td,{children:`Milliseconds after the oldest record not yet in a snapshot before a new one is taken.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`LEASE_DURATION`})}),_jsx(_components.td,{\"data-numeric\":`true`,children:_jsx(_components.code,{children:`30`})}),_jsx(_components.td,{children:`Seconds a connection holds the snapshot lease before others may take it over.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`S2_BATCH_SIZE`})}),_jsx(_components.td,{\"data-numeric\":`true`,children:_jsx(_components.code,{children:`8`})}),_jsx(_components.td,{children:`Maximum number of records per append batch.`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`S2_LINGER_TIME`})}),_jsx(_components.td,{\"data-numeric\":`true`,children:_jsx(_components.code,{children:`50`})}),_jsx(_components.td,{children:`Milliseconds to wait for more records before sending an append batch.`})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`client-integration`,children:`Client integration`}),`\n`,_jsxs(_components.p,{children:[`Connect with the `,_jsx(_components.a,{href:`https://github.com/yjs/y-websocket`,children:`y-websocket`}),` provider. The server URL ends in `,_jsx(_components.code,{children:`/ws`}),`, and the room is passed both as the room name and as the `,_jsx(_components.code,{children:`room`}),` parameter:`]}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`13`,language:`javascript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`javascript`,children:_jsxs(_components.code,{language:`javascript`,numberOfLines:`13`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`import`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` *`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` as`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` Y`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` from`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'yjs'`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`import`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` { `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`WebsocketProvider`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` } `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`from`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'y-websocket'`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`const`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` room`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'room-name'`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`const`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` doc`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:` new`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` Y`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`Doc`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`()`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`const`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` provider`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:` new`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:` WebsocketProvider`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'wss://your-worker.your-subdomain.workers.dev/ws'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` room`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` doc`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` {`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` params:`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` { `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`room`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`yauth:`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` 'your-auth-token'`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` }`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` }`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`Presence works through the provider’s `,_jsx(_components.code,{children:`awareness`}),`, as with any y-websocket server.`]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[`Y-S2 does not authenticate clients yet: any non-empty `,_jsx(_components.code,{children:`yauth`}),` value is accepted, and `,_jsx(_components.code,{children:`/auth/token`}),` returns a fixed demo token. Add your own authorization before using it for anything but demos. Anyone who can reach the Worker can read and write any room, and create new rooms in your basin.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`resources`,children:`Resources`}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsx(_components.li,{children:_jsx(_components.a,{href:`https://s2.dev/demos/y-s2`,children:`Demo`})}),`\n`,_jsx(_components.li,{children:_jsx(_components.a,{href:`https://github.com/s2-streamstore/y-s2`,children:`GitHub repository`})}),`\n`,_jsx(_components.li,{children:_jsx(_components.a,{href:`https://s2.dev/blog/durable-yjs-rooms`,children:`Behind y-s2: serverless multiplayer rooms`})}),`\n`,_jsx(_components.li,{children:_jsx(_components.a,{href:`https://docs.yjs.dev`,children:`Yjs documentation`})}),`\n`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Y-S2","sidebarTitle":"Yjs","description":"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.","href":"/integrations/y-s2"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"How it works","slug":"how-it-works","depth":2,"children":[]},{"title":"Prerequisites","slug":"prerequisites","depth":2,"children":[]},{"title":"Deploy","slug":"deploy","depth":2,"children":[]},{"title":"Configuration","slug":"configuration","depth":2,"children":[]},{"title":"Client integration","slug":"client-integration","depth":2,"children":[]},{"title":"Resources","slug":"resources","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":["/docs/_mintlify/custom/streamstore/00ba98d0ea337170/asciinema-init.js","/docs/_mintlify/custom/streamstore/9345475b878fd043/asciinema-player.min.js","/docs/_mintlify/custom/streamstore/cf00df61fe9b4388/copy-page-markdown.js"],"subdomain":"streamstore","siteName":"S2","title":"Y-S2 - S2","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{}},"lastModified":"2026-10-22T00:06:17.316Z","originalFileLocation":"integrations/y-s2.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.\">\u003cmeta data-mint-head name=\"application-name\" content=\"S2\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Y-S2 - S2\">\u003cmeta data-mint-head property=\"og:description\" content=\"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"S2\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://s2.dev/docs/integrations/y-s2\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://streamstore.mintlify.app/_mintlify/api/og/v2?division=Sync\u0026amp;title=Y-S2\u0026amp;description=Build+durable+Yjs+collaborative+editing+with+y-s2%2C+using+S2+streams+for+updates+and+Cloudflare+R2+for+snapshots.\u0026amp;theme=0bf2fcc0d6937ccab2e1eca2\u0026amp;signature=f61d4b355c44917f1e2568f93fa4ea99899b5b8c4381d748d4500e95ca896391\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Y-S2 - S2\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://streamstore.mintlify.app/_mintlify/api/og/v2?division=Sync\u0026amp;title=Y-S2\u0026amp;description=Build+durable+Yjs+collaborative+editing+with+y-s2%2C+using+S2+streams+for+updates+and+Cloudflare+R2+for+snapshots.\u0026amp;theme=0bf2fcc0d6937ccab2e1eca2\u0026amp;signature=f61d4b355c44917f1e2568f93fa4ea99899b5b8c4381d748d4500e95ca896391\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/docs/mintlify-assets/_mintlify/favicons/streamstore/A9aRpo3I3-CnbGBz/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"S2\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#252525\">\u003clink data-mint-head rel=\"canonical\" href=\"https://s2.dev/docs/integrations/y-s2\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/docs/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/docs/integrations/y-s2.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://s2.dev/#organization\",\"name\":\"S2\",\"url\":\"https://s2.dev\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/streamstore/2KVWF55FbBFAHG9D/logo/s2-black.png?fit=max\u0026auto=format\u0026n=2KVWF55FbBFAHG9D\u0026q=85\u0026s=a21a60dc6c4cc961bb234a8eba956217\"}},{\"@type\":\"WebSite\",\"@id\":\"https://s2.dev/docs#website\",\"name\":\"S2\",\"url\":\"https://s2.dev/docs\",\"publisher\":{\"@id\":\"https://s2.dev/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://s2.dev/docs/integrations/y-s2#webpage\",\"url\":\"https://s2.dev/docs/integrations/y-s2\",\"name\":\"Y-S2\",\"description\":\"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.\",\"dateModified\":\"2026-10-22T00:06:17.316Z\",\"isPartOf\":{\"@id\":\"https://s2.dev/docs#website\"},\"breadcrumb\":{\"@id\":\"https://s2.dev/docs/integrations/y-s2#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://s2.dev/docs/integrations/y-s2#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Sync\",\"item\":\"https://s2.dev/docs/integrations/livestore\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Y-S2\",\"item\":\"https://s2.dev/docs/integrations/y-s2\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://s2.dev/docs/integrations/y-s2#article\",\"headline\":\"Y-S2\",\"name\":\"Y-S2\",\"description\":\"Build durable Yjs collaborative editing with y-s2, using S2 streams for updates and Cloudflare R2 for snapshots.\",\"url\":\"https://s2.dev/docs/integrations/y-s2\",\"mainEntityOfPage\":{\"@id\":\"https://s2.dev/docs/integrations/y-s2#webpage\"},\"dateModified\":\"2026-10-22T00:06:17.316Z\",\"publisher\":{\"@id\":\"https://s2.dev/#organization\"},\"isPartOf\":{\"@id\":\"https://s2.dev/docs#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"S2\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"S2","nav":{"global":{"anchors":[{"anchor":"Blog","icon":"newspaper","href":"https://s2.dev/blog"},{"anchor":"Talk to us","icon":"comments","href":"https://s2.dev/contact"}]},"tabs":[{"tab":"Start","pages":[{"title":"Start","href":"/intro"}]},{"tab":"Concepts","pages":[{"title":"Concepts","href":"/concepts/records"}]},{"tab":"Platform","pages":[{"title":"Platform","href":"/platform/cells"}]},{"tab":"SDKs","pages":[{"title":"SDKs","href":"/sdk/languages"}]},{"tab":"CLI","pages":[{"title":"CLI","href":"/cli/overview"}]},{"tab":"API","pages":[{"title":"API","href":"/api/protocol"}]},{"tab":"Integrations","pages":[{"title":"Integrations","sidebarTitle":"Overview","href":"/integrations/overview"},{"group":"Protocols","pages":[{"title":"Redis / Valkey clients","tag":"Beta","href":"/integrations/redis-streams"},{"title":"OpenTelemetry","href":"/integrations/otlp"}]},{"group":"AI SDKs","pages":[{"title":"Vercel AI SDK","href":"/integrations/vercel-ai-sdk"},{"title":"TanStack AI","href":"/integrations/tanstack-ai"},{"title":"Anthropic","href":"/integrations/anthropic"},{"title":"Mastra","href":"/integrations/mastra"}]},{"group":"Pipelines","pages":[{"title":"Bento","href":"/integrations/bento"},{"title":"Streamling","href":"/integrations/streamling"},{"title":"Feldera","href":"/integrations/feldera"},{"title":"Goldsky","href":"/integrations/goldsky"}]},{"group":"Sync","pages":[{"title":"LiveStore","description":"Use S2 as a durable backend for LiveStore's event-sourced sync engine.","href":"/integrations/livestore"},{"title":"Y-S2","sidebarTitle":"Yjs","href":"/integrations/y-s2"}]}]},{"tab":"Updates","pages":[{"title":"Updates","href":"/updates"}]}]},"title":"Y-S2 - S2","buildId":"dpl_AUojGTPodKNPTVaxVPcxnNvkSVqo","deploymentId":"dpl_AUojGTPodKNPTVaxVPcxnNvkSVqo","dynamics":[],"chunks":[]}