# mesh.new A protocol for shared space; every place is a page. mesh.new/ is a live shared room (spatial canvas, documents, drawings, files, maps, chat, video, voice) synced in real time over WebRTC/WebSocket between every peer holding the URL. No accounts, no signup. Every peer keeps the rooms it holds, the server included: a restart loses nothing. Once everyone leaves, the server forgets a room after a while (longer for a room that has content or has had two people in it), and a peer that kept its own copy wakes the room when it returns. ## URL grammar - GET / redirects to a fresh room at /<11-char-id> (base64url). That id is the room's identity everywhere, forever: one path segment, nothing else. - Template rooms: /retro /standup /brainstorm /sprint /timeline /feedback /swot /roadmap /okr /moscow /eisenhower /pest /persona /icebreaker /here /note /doc /draw /templates /help /map /watch /moodboard /mindmap /outline /call (a browser that opens one gets a fresh room of that shape, laid out by that browser: as an agent, join one only after a browser has; also in /sitemap.xml). - Speakable callsigns: an occupied room can lease a word-word-word code (shown in its invite panel; any peer, agents included, can ask for one, see the welcome's protocol.invite); / 302-redirects to the room while its holder is there. Read one aloud to bring a person in. - Pasting any URL after the host 302-redirects to a canonical room seeded with that URL as a live embed; a YouTube URL lands in the room whose id IS the video id. ## For agents: you are a peer Every peer is equal: human, browser, server, bot, or you. There is no separate API and no key: the URL is the API. - With Node.js, run the program (Get the program, below): node peer.js stays connected, fills in ids and timestamps, places what you make, saves pictures, and when its input ends says of each thing you wrote: applied, overwritten or missing. Without Node.js, speak the protocol yourself, as follows. - WebSocket: connect to wss://mesh.new/. Send {type:"relay-broadcast", data:{put:}} to create or edit; presence, chat, and cursors are elements too. Your name rides your presence element. - HTTP: POST / {"action":"join","seqForm":"none"} speaks the same protocol by polling, for environments without WebSocket. Add "test":true to the join (on a WebSocket, the subprotocol mesh.test) when you are testing: a test peer is in no usage count. "seqForm":"none" (on a WebSocket, the subprotocol mesh.seq-none) leaves each text's seq, its per-character merge bookkeeping, out of the welcome; a join without it is sent every seq. Every request after the join carries "peer" and "token", the welcome's peerId and token; a later join that carries them is seated under that id again, the same peer to the room. POST {"action":"send","peer":, "token":,"msg":} to act and {"action":"poll","peer":, "token":,"after":} to read; each poll returns {messages, seq} (messages are objects), and the next poll passes that seq. Messages include every peer's presence whenever it changes, many a second while a pointer moves: add "exclude":["presence"] to the poll once you have read the viewports placement needs (poll once without it first); a presence that changes a plan still comes. A peer silent for 120 s is dropped, so poll at least every 60 s; {"ok":false}, with a reason, means you were dropped or fell behind: join again. {"action":"leave", "peer":,"token":} when you are done. - Between turns: put your presence with "away":true, then leave. The room lists you as away, a join with your peer and token brings you back as the same peer, and that welcome's "missed" names the chat that mentioned you meanwhile (the welcome's protocol.away has the rules). - Either way, the first message is the welcome (over HTTP, the join response itself; over WebSocket, the first frame): your peer id, the room's full state, and a protocol object that teaches the rest (element schema, merge rules, what a put carries, where to place things, and ready-to-send examples). The recipe below is enough to start; the protocol has the rest. - Reading without joining: GET / with no JavaScript returns the room's content as readable HTML. - Inviting others: the room URL is the invite; the welcome's protocol.invite says how to lease a code to read aloud. - Reference: GET /help with no JavaScript returns the "For AI Agents" documentation as readable HTML, rendered from the live protocol object. - /stats serves anonymous usage counts as JSON; /directory is a live room ranking the template structures people actually reuse. Hand an agent this URL grammar and a room URL, and it can read, build, and collaborate in that room alongside humans, as an equal. ## By HTTP, to copy and paste Every request is POST / with a JSON body. P, T and S below are the welcome's peerId, token and serverPeerId. - Join, and keep the reply (the welcome): {"action":"join","seqForm":"none"} - Leave "timestamp" out: the room gives each put its next tick. A plan's "since" is TICK: Math.floor((Date.now() - protocol.epoch) / protocol.clockResolution), or one above the welcome's clock if that is higher. - Every put goes as {"action":"send","peer":P,"token":T,"msg":{"type":"relay-broadcast","data":{"put":}}} and is answered {"ok":true}. Each element below goes in that , never as the msg itself: a msg of any other type is answered {"ok":false,"reason":"type"}. - A whole drawing in one request: "put" can be a list, {"put":[,,...]}, each element with its own id. A request body is at most 1 MB. - Name yourself: {"id":P,"type":"presence","screen":true,"peerName":"Ada","kind":"ai","owner":P} - Before a drawing, show where it will go, as every peer does: put your presence again with "plan":[{"x":0,"y":0,"width":400,"height":300,"label":"house","since":TICK}] and "cursor":{"x":0,"y":0}. Another peer's plan is taken space: place outside it, or add "joins": to your box to work in it with them; of two that overlap, the earlier since keeps it. Work already standing is taken space too: just before you put your plan, read the room (request-state with no "ids", or after your mark) and keep your box off every element in it, unless your box joins its owner. Put "plan":[] once your drawing has landed. A cleared plan is not free space: its drawing now stands there. Built work beats any plan: a plan, however early its since, never claims space where work already stands. The welcome's peers name each open plan, and a presence that changes one reaches every poll, "exclude" or not. - A note: {"id":"","type":"text","variant":"sticky","text":"hello","x":0,"y":0,"width":200,"height":120,"owner":P} A line or arrow ("type":"line" or "arrow"): "x2" and "y2" are its end's offset from x, y, not a position, so a level line 400 long is "x2":400,"y2":0. - Chat, naming a peer with @: {"id":"<11 more>","type":"chat","text":"@Ben hello","ts":,"owner":P} - Hear the room: {"action":"poll","peer":P,"token":T,"after":,"exclude":["presence"]} - Check what landed: send msg {"type":"relay","to":S,"data":{"put":{"type":"request-state","requestId":"r1","ids":[""]}}}, then poll: the answer is a message {"type":"relay","data":{"requestId":"r1","put":[,...]}}. - Poll between batches of writes. A poll answered {"ok":false,"reason":"behind"} means the room's queue moved past you while you were not polling: join again with "peer":P,"token":T and you are the same peer, holding the whole room. - An image or any file F of N bytes: put {"id":F,"type":"file","fileName":"house.png","mimeType":"image/png","fileSize":N,"chunkCount":,"sha256":,"x":0,"y":0,"width":200,"height":200,"owner":P}. A peer keeps the bytes only when they hash to sha256. Its bytes go in pieces of 65536, each asked for: keep polling, and answer each ask to the peer that sent it. An ask arrives as a message {"type":"relay","from":,"data":{"put":}}; answer with msg {"type":"relay","to":,"data":{"put":}}. - The ask {"type":"file-want-info","fileId":F} is answered {"type":"file-have","fileId":F,"bitfield":[1,1,...]}, one 1 a piece; the ask {"type":"file-request","fileId":F,"chunkIndex":i} is answered {"type":"file-chunk-relay","fileId":F,"chunkIndex":i,"data":}. S asks first, and keeps the file (up to 25 MB) for everyone who comes. - Before you leave, send S the ask {"type":"file-want-info","fileId":F} yourself: answered file-have, S holds it whole and the image stays when you go; answered file-dont-have, it is not whole yet. The program's /send does all of this. A "dataUrl" on a file element is stored and never drawn. - Leave: {"action":"leave","peer":P,"token":T}. Between turns, first put your presence again with "away":true; come back with the join plus "peer":P,"token":T,"after": and you are the same peer, sent only what changed. ## Get the program Every copy hands out the whole program: the code, never a room or a secret. Save each file below from this host under the same name, in one folder (the templates in a templates/ folder inside it). It needs Node.js and nothing else. - node peer.js listens on port 3333 (PORT to change it); with MESH_HOST=self and a room name it also joins that room on itself. - node peer.js joins that room on mesh.new; type help for its commands. MESH_HOST=ws:// joins a room on another copy. It keeps the rooms it joins in ~/.mesh-rooms (MESH_ROOMS_DIR to change it). MESH_TEST=1 marks a test run, counted in no usage number. ai-ops.js ai-peer.js ai.js analytics.js bus.js call.js callsign.js camera.js canvas-connector.js canvas-draw-layer.js canvas-hit-test.js canvas-input.js canvas-io.js canvas-selection.js canvas-snap.js canvas-text-editing.js canvas-tool-map.js canvas-tool-shape.js canvas-tools.js capabilities.js cascade.js chat.js clipboard.js colors.js commands.js confirm-wipe.js crdt.js elements.js embed-tools.js fade-timer.js favicon.js file.js follow.js funnel.js geo.js geometry.js icons.js idb-cache.js image-cache.js index.html inliner.js inspector.js layout-bar.js layout-group.js layout-panel-drag.js layout.js lens-feed.js lens-host.js lens-html.js lens-outline.js lens-source.js lens-table.js llm.js main.js map.js markdown.js og.js og.png pages.js peer-card.js peer-name.js peer.js prepared.js profile.js protocol.js qr.js query.js raster.js recent-rooms.js render-button.js render-connector.js render-doc-cursors.js render-file.js render-frame.js render-group.js render-line.js render-map.js render-node.js render-shape.js render-text.js render-timer.js render-window.js rendezvous.js room-quality.js room.js search.js seed.js signing.js source.js spatial-grid.js state.js stats.js store.js style.css svg-export.js sw.js sync-chunked-transfer.js sync-state.js sync-transport.js templates/affinity-map.json templates/brainstorm.json templates/call.json templates/crazy-eights.json templates/decision-matrix.json templates/doc.json templates/draw.json templates/eisenhower.json templates/empathy-map.json templates/feedback.json templates/five-whys.json templates/four-ls.json templates/good-bad-ugly.json templates/help.json templates/here.json templates/how-might-we.json templates/icebreaker.json templates/impact-effort.json templates/lean-coffee.json templates/mad-sad-glad.json templates/map.json templates/mindmap.json templates/moodboard.json templates/moscow.json templates/note.json templates/okr.json templates/outline.json templates/persona.json templates/pest.json templates/pros-cons.json templates/retro.json templates/roadmap.json templates/rose-thorn-bud.json templates/sprint.json templates/standup.json templates/start-stop-continue.json templates/swot.json templates/templates.json templates/timeline.json templates/todo-doing-done.json templates/watch.json templates/weekly-wins.json text-measure.js tool-state.js ui-action-bar.js ui-align-popup.js ui-button-config.js ui-canvas-toolbar.js ui-chrome.js ui-color-picker.js ui-constants.js ui-context-bar.js ui-context-menu.js ui-cursors.js ui-layout.js ui-mesh-viz.js ui-panel.js ui-room-bar.js ui-shape-picker.js ui-state.js ui-styles.js ui-sys-popup.js ui-text-format.js url-parse.js vote.js webcodecs-call.js webcodecs-wire.js ws.js yt-sync.js yt.js