<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Esteban Fuster Pozzi</title>
    <description>The latest articles on DEV Community by Esteban Fuster Pozzi (@estebanrfp).</description>
    <link>https://dev.to/estebanrfp</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2934186%2F79c95f8d-8996-4696-a11f-a8bfef73dd00.jpeg</url>
      <title>DEV Community: Esteban Fuster Pozzi</title>
      <link>https://dev.to/estebanrfp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/estebanrfp"/>
    <language>en</language>
    <item>
      <title>A GitHub With No Server: dCode, a Peer-to-Peer Code Editor and Code Host in One Page, Just GenosDB</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Sun, 06 Sep 2026 22:51:19 +0000</pubDate>
      <link>https://dev.to/estebanrfp/a-github-with-no-server-dcode-a-peer-to-peer-code-editor-and-code-host-in-one-page-just-genosdb-j38</link>
      <guid>https://dev.to/estebanrfp/a-github-with-no-server-dcode-a-peer-to-peer-code-editor-and-code-host-in-one-page-just-genosdb-j38</guid>
      <description>&lt;p&gt;Two people open the same file. Both type. Their carets are on each other's screens. One of them commits; the other forks, proposes, and the owner merges. Every commit in the timeline can be run. And there is no server anywhere — not for the editing, not for the history, not for the merge.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;&lt;a href="https://estebanrfp.github.io/dCode/" rel="noopener noreferrer"&gt;dCode&lt;/a&gt;&lt;/strong&gt;: a real-time collaborative code editor and a GitHub-style code host in one page, for projects that are &lt;strong&gt;one HTML file&lt;/strong&gt; — HTML, CSS and JavaScript together, the way CodePen or JSFiddle hold a pen. It is four files of plain JavaScript, about seven hundred lines of application, on top of &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt;, a peer-to-peer graph database that lives in the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focodnb6rogb2b72kje86.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focodnb6rogb2b72kje86.png" alt="dCode with two peers: on the left the shared buffer with line numbers and syntax colours, Bob's named caret on line 15, the HTML · CSS · JS views above it; on the right the page running as it is typed, and under it the History tab with the commit graph and the diff of the selected commit." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Alice's window. Bob's caret is on line 15; the page on the right re-ran a moment after his last keystroke; the timeline below is the history of &lt;code&gt;main&lt;/code&gt;, and every row of it runs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the sequel to &lt;a href="https://genosdb.com/genosdb-collaborative-editing" rel="noopener noreferrer"&gt;Real-Time Collaborative Editing Without a CRDT&lt;/a&gt;, which showed that a block editor — one node per paragraph — reaches the live experience of Yjs and Automerge without a CRDT library, because a graph database with ownership, ordering and an ephemeral channel already carries the pieces. This article takes the same technique to code, and then goes where an editor alone cannot: &lt;strong&gt;the things GitHub runs servers for&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitHub Runs Servers For
&lt;/h2&gt;

&lt;p&gt;A code host is a list of guarantees, and each of them is usually a server's job.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;On GitHub, on CodePen&lt;/th&gt;
&lt;th&gt;On dCode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A server holds the repositories, a server runs the collaboration&lt;/td&gt;
&lt;td&gt;Every visitor holds the graph; peers sync it directly over WebRTC, live typing included&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An account is a row in a database&lt;/td&gt;
&lt;td&gt;An identity is a key pair on your device: a mnemonic recovers it, a passkey keeps it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A shared editor needs a CRDT library and a relay&lt;/td&gt;
&lt;td&gt;A line is a node: two people on different lines never collide, two on the same line keep both edits when they touch different places, carets ride an ephemeral channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Branch protection is a server rule&lt;/td&gt;
&lt;td&gt;A branch is a node its owner owns: only the owner, and the addresses granted &lt;code&gt;write&lt;/code&gt;, can move its head — refused on every peer otherwise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A commit is trusted because the server says so&lt;/td&gt;
&lt;td&gt;A commit's id begins with its author's address and ends with a hash of its content: nobody else can create it, rewrite it or delete it, anywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A pull request's state is a column&lt;/td&gt;
&lt;td&gt;Whether a proposal is merged is read from the graph by every peer: the proposed commit is an ancestor of the target's head, or it is not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History is browsed&lt;/td&gt;
&lt;td&gt;History &lt;strong&gt;runs&lt;/strong&gt;: a project is one HTML file, so any commit opens in a sandboxed frame as it was&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline is an error&lt;/td&gt;
&lt;td&gt;A commit made with no peer in sight waits on this device's disk and lands when a path exists, signed like any other&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every row on the right is a property of the graph, not a feature of the application. The application is short because it has almost nothing to enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Layers, One Graph
&lt;/h2&gt;

&lt;p&gt;dCode keeps two kinds of state in one GenosDB graph, and the difference between them is the whole design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8xlsg9kerphgaldde8l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8xlsg9kerphgaldde8l.png" alt="Two layers, one graph. Left, the buffer is shared: every line of the code is a plain node with a text and a fractional order key, edited by everyone on the branch, with two named carets on two lines. Right, the history is owned: a repository node, a branch node with a head, a chain of commit nodes each owned by its author, and a pull request node from one branch into another. One db.map subscription feeds both layers." width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left, the buffer: plain nodes anyone on the branch may write. Right, the history: nodes only their owner may write. One subscription feeds both.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The buffer is shared.&lt;/strong&gt; Every line of a branch's code is a plain node — &lt;code&gt;{ type: "line", branch, text, order }&lt;/code&gt; — that anyone on the branch may rewrite, split, merge, move or remove. It is the working tree, and it is live: what one person types, the others see character by character.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The history is owned.&lt;/strong&gt; A repository, a branch, a commit and a pull request are nodes created with GenosDB's ACL layer, &lt;code&gt;db.sm.acls.set&lt;/code&gt;. The creator becomes the owner, and the engine refuses anyone else's edit or deletion — on every peer, live or through catch-up. Nothing in the history can be changed by someone who did not write it.&lt;/p&gt;

&lt;p&gt;Both layers arrive through one subscription:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;branch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="c1"&gt;// lines go straight to the editor; everything else redraws the timeline, the branches, the pull requests&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Branches, the timeline, the diff of a commit and the state of every pull request are pure functions of that store. Nothing ticks over the wire; nothing is asked of anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Editor Is the Block Editor, for Code
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://estebanrfp.github.io/gdb/examples/block-editor.html" rel="noopener noreferrer"&gt;block editor&lt;/a&gt; made a document out of paragraph nodes. dCode makes a file out of line nodes, and inherits every property that article argued for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line-level last-write-wins.&lt;/strong&gt; Two people on different lines never collide, because there is no shared node to fight over. Two carets on the &lt;em&gt;same&lt;/em&gt; line keep both edits when they touch different places; only the same span at the same instant is the later writer's — and they can see each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fractional order keys.&lt;/strong&gt; A line inserted between keys &lt;code&gt;13&lt;/code&gt; and &lt;code&gt;15&lt;/code&gt; gets a key in the gap. Two peers inserting at the same place mint different keys, both survive, and every peer sorts them identically: a sorted &lt;code&gt;db.map&lt;/code&gt; breaks ties on the node id, in the direction of the sort. Enter splits a line into two nodes and keeps the indentation; Backspace at the start of a line merges it into the one above; Alt+↑/↓ moves a line by giving it a new key; a multi-line paste mints all its keys in one batch, so a thousand pasted lines cost the precision of one insert, not one halving per line. The pattern, its float64 limit and the engine's rule are written up in the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/ordered-lists.md" rel="noopener noreferrer"&gt;Ordered Lists guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Awareness is ephemeral.&lt;/strong&gt; Carets, selections and the line being typed travel on a &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-guide.md" rel="noopener noreferrer"&gt;GenosRTC&lt;/a&gt; channel that never touches the database — which is also how Yjs does awareness. Keystrokes are coalesced to one message per frame; the debounced &lt;code&gt;put&lt;/code&gt; remains the truth that persists, repairs and survives offline.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The truth: one node per line, written 250 ms after the last keystroke.&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;line&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;lineId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// The experience: the line being typed, keystroke by keystroke, on the room's channel.&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;presence&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lineId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things a code editor needs that a document did not: &lt;strong&gt;line numbers&lt;/strong&gt;, which are positions derived from the order and never stored, and &lt;strong&gt;syntax colours&lt;/strong&gt;, painted per line by the language the line is in — HTML, the CSS inside &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, the JavaScript inside &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. The same pass that finds those regions gives the &lt;strong&gt;HTML · CSS · JS views&lt;/strong&gt;: filters over the one file, showing the &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; block or the &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block with the file's own line numbers. The same nodes, edited and synced the same way; a line born in the CSS view is inside &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; and stays visible there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0j9k5y2nqh6scmzhg10.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0j9k5y2nqh6scmzhg10.png" alt="The CSS view of the same file: only lines 7 and 8, the two rules inside the style block, with their real line numbers, coloured, the page still running on the right." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The CSS view: two lines, numbered 7 and 8 because that is where they are in the file. An edit here is an edit of the same node the HTML view shows.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The page on the right re-runs a moment after the last change, in a sandboxed frame with no access to the host page. &lt;strong&gt;Download .html&lt;/strong&gt; saves the buffer as one file — HTML, CSS and JavaScript together — and every commit in the timeline downloads as it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Commit That Names Itself
&lt;/h2&gt;

&lt;p&gt;A commit is a node holding the whole file, a message, its parents and the branch it was made on. What makes it &lt;em&gt;history&lt;/em&gt; rather than data is its id.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjx6dhp04vlyms18qvku.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmjx6dhp04vlyms18qvku.png" alt="A commit that names itself: its id is the author's address, a colon, and a SHA-256 of the repository, the parents, the message, the content and the time. Nobody else can create, rewrite or delete it on any peer, and the author cannot rewrite it without producing a different id." width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The id is built by the author and enforced by every peer: the address says who may sign for it, the hash says what it holds.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;commit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GenosDB's authorship gate has a rule for ids that begin with an address: on a peer that never saw the node, an operation on that id is accepted only when it is signed by that address. So nobody else can create a commit under Alice's name, on any peer, ever. And because the rest of the id is a hash of what the commit holds, Alice cannot rewrite it either: change one byte and you have a different commit, while the old one stays where every peer keeps it. Immutable history, with no server to keep it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Branch Only Its Owner Moves
&lt;/h2&gt;

&lt;p&gt;A branch is an owned node with a &lt;code&gt;head&lt;/code&gt;. The owner moves it; the owner can grant &lt;code&gt;write&lt;/code&gt; to collaborators with &lt;code&gt;db.sm.acls.grant&lt;/code&gt;; everyone else's write of that node is refused by every receiver. That is GitHub's branch protection, and it costs the application zero lines: the engine enforces it.&lt;/p&gt;

&lt;p&gt;What does someone without write do? They edit the shared buffer like anyone — and when they commit, the commit goes to &lt;strong&gt;a branch of their own&lt;/strong&gt;, forked from the head they stood on, with a copy of the buffer. A fork is a branch you own in someone else's repository. Then they propose.&lt;/p&gt;

&lt;p&gt;The test suite includes the negative, because a zero-trust claim is only as good as its refusal. Bob, from the browser console, writes Alice's branch node with his own head, past the application. His own graph believes him — a tampered client can always lie to itself. Every other peer refuses the operation, and the proof is not silence: Bob's next honest commit lands on Alice's screen while &lt;code&gt;main&lt;/code&gt; has not moved.&lt;/p&gt;

&lt;p&gt;Two collaborators pushing to one head at the same moment is the other case a server usually arbitrates. Here the hybrid logical clock keeps one head; the other commit stays in the graph, behind, and merges like any other — the same outcome as a rejected non-fast-forward push, resolved by the clock instead of an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests Nobody Marks as Merged
&lt;/h2&gt;

&lt;p&gt;A pull request is a node its proposer owns: from one branch, into another, with the commit it proposes. The proposer updates it when their branch moves, or withdraws it. The target's owner merges it. And &lt;em&gt;merged&lt;/em&gt; is never written by anyone: every peer derives it from the graph — the proposed commit is an ancestor of the target's head, or it is not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzgcqq78kwk2bf5qetrvi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzgcqq78kwk2bf5qetrvi.png" alt="How a pull request is merged, in three cases: a fast-forward when the target's head is an ancestor of the proposal; a clean line-based three-way merge from the newest common ancestor, committed with two parents; and a conflict, whose markers land in the target's shared buffer for the owner to resolve, the commit made from the resolution being the merge." width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Three merges, one rule: the target's owner writes, every peer reads. The shared buffer follows the new head in all three.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When the target's head is an ancestor of the proposal, the merge is a fast-forward: one write on the branch node. Otherwise a line-based three-way merge runs from the newest common ancestor — a region changed on one side takes that side, changed the same way on both is taken once — and the owner commits the result with both heads as parents. Adjacent changes are one region and a conflict, as in git.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh535t9l85mfdk25p2uqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh535t9l85mfdk25p2uqx.png" alt="The Pull requests tab after a merge: Bob's proposal from bob/main into main reads as merged, with its commit, its author and its target." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After the merge. Nobody wrote "merged" anywhere: Bob's commit is now an ancestor of main's head, and every peer reads the same thing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A conflict is the interesting case, because the buffer is shared. The marked text lands in the target branch's buffer — on every screen, Bob's included — and the owner resolves it in the editor, with everyone watching. The commit made from the resolution has two parents and is the merge. Markers left in the file are refused, in words.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F001ful01i7fwqdebvvg3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F001ful01i7fwqdebvvg3.png" alt="A conflict in the shared buffer: the conflict markers around two versions of the heading, the merge banner above the editor, and the merge message prefilled." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Both changed the heading. The markers are in main's buffer for everyone to see; the next commit on main will be the merge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38pc91pmse1jdwxm5ct5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F38pc91pmse1jdwxm5ct5.png" alt="The History tab after the merges: the commit graph with two lanes, main and bob/main, the merge commit joining them, the chips showing which branch sits on which commit, and the diff of the selected commit." width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The timeline is a graph in the margin: one lane per branch, one row per commit, and every row runs.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline Is a Commit Like Any Other
&lt;/h2&gt;

&lt;p&gt;Close the network and keep working. The buffer is on this device's disk — GenosDB persists the graph in the browser's Origin Private File System — and a commit made with no peer in sight is a commit: signed, owned, waiting. When a path exists again, it travels through the delta sync and lands on every peer that accepts it under the same rules as a live one. There is no "offline mode" in the application, because there is nothing for it to do: the same code path that handles a live write handles a write that spent an hour on disk.&lt;/p&gt;

&lt;p&gt;This is the behaviour the tests exercise with a peer pointed at a relay nobody listens on: it creates a repository and commits alone, comes back on the live relay, and a second visitor finds the repository, the commit and the buffer, line for line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Private Repositories: The Engine Keeps the Key
&lt;/h2&gt;

&lt;p&gt;Everything above is readable by the room, on purpose: a public repository on dCode is public the way a public repository on GitHub is. A private one is a different promise, and a system with no server has to keep it with cryptography, because there is nobody to ask who is asking.&lt;/p&gt;

&lt;p&gt;GenosDB has the primitive. &lt;code&gt;db.sm.put&lt;/code&gt; writes an &lt;strong&gt;encrypted node&lt;/strong&gt;: the value is sealed with a content key, and that key is wrapped once per reader — an envelope per address — inside the node itself. &lt;code&gt;grant&lt;/code&gt; on it adds an envelope; &lt;code&gt;revoke&lt;/code&gt; removes one and rotates the key. dCode uses exactly one such node per private repository: its &lt;strong&gt;vault&lt;/strong&gt;, holding the repository's key ring, newest key first.&lt;/p&gt;

&lt;p&gt;From there the application does the small part. Tick &lt;em&gt;Private&lt;/em&gt; when creating a repository and the owner's session mints a key and puts it in the vault. Every line of the buffer and every commit is then sealed with that key — AES-GCM, in the browser — before it is written as an ordinary node, and the live keystrokes on the ephemeral channel are sealed the same way. The name, the description, the branch names, the commit messages and the pull request titles stay readable, so the timeline still makes sense to someone who cannot open the code. A &lt;strong&gt;member&lt;/strong&gt; is an address the owner grants &lt;code&gt;read&lt;/code&gt; on the vault: the engine wraps the key for them, their browser fetches it, opens what is already on its disk and whatever arrives afterwards, and the editor appears — the grant reaches their page on its own, through the same subscription that does everything else. Revoking a member turns two keys at once: the engine rotates the vault's, and the application adds a new key to the repository's ring, so what is written afterwards never opens for them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0sabyheaeufg5l2870m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc0sabyheaeufg5l2870m.png" alt="Alice's page of a private repository in dCode: the private badge beside the name, the Members section in the Branches tab listing alice as owner and bob with read and a Revoke button, and the notice that bob holds the key now." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two details are worth the space. First, &lt;strong&gt;the code is opened in memory and never written back in clear&lt;/strong&gt;: a member's disk holds the same ciphertext as everyone else's, and the test suite reads the OPFS file of every peer — the owner's included — to prove it. Second, &lt;strong&gt;the authority is not a member&lt;/strong&gt;: the constitution's superadmin, who can restrict any identity, meets the same locked door as a stranger. In a system with no server, "private" cannot mean "the admin can see it"; it means whoever holds an envelope, and nobody else.&lt;/p&gt;

&lt;p&gt;What it does not do, said plainly: a revocation is forward-only — what a former member already read, they read — and the ordinary nodes that carry the ciphertext can still be vandalised by anyone in the room, since writing to a shared buffer is free. Both are the trade-offs a shared document has too; the code is no less private for them, and the history stays owned and signed either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constitution: Writing Is Free, Nothing Owned Is Touchable
&lt;/h2&gt;

&lt;p&gt;dCode is governed the way &lt;a href="https://genosdb.com/genosdb-hacker-news-clone-no-server" rel="noopener noreferrer"&gt;dNews&lt;/a&gt; is: by a constitution every peer runs, rendered verbatim on its own page. Its roles are short, because the ownership rules above do the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;guest&lt;/strong&gt; — everyone, from the first second: reads everything, writes nodes of its own and edits or removes the lines of any shared buffer. Nothing owned by someone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;restricted&lt;/strong&gt; — lost the right to write. Its repositories, branches and commits stay exactly where they are, signed by it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;superadmin&lt;/strong&gt; — the authority. Its only power is to restrict an identity, with its signature. It cannot touch a repository, a branch or a commit it does not own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing is free from the first second — as on any code host, you sign up and you push — because on an owned node a write or a deletion is only ever the owner's. The one permission worth a sentence is &lt;code&gt;delete&lt;/code&gt;: a shared buffer must be able to lose a line, so guests may remove plain nodes; owned nodes stay gated by ownership. Everything GenosDB's &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/zero-trust-security-model.md" rel="noopener noreferrer"&gt;zero-trust model&lt;/a&gt; enforces — signatures on every operation, verification on every peer, roles valid only with the authority's signature — applies here without a line of application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Deliberately Does Not Do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interleaving inside one span.&lt;/strong&gt; Two people on the same line keep both edits when they touch different places: the engine re-applies the losing write over the winner, and the editor lands it under the caret. Only the same span at the same instant is the later writer's, with the carets in view. That is the dividing line the &lt;a href="https://genosdb.com/genosdb-collaborative-editing" rel="noopener noreferrer"&gt;previous article&lt;/a&gt; draws, and it holds for code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Folders, binaries, git interoperability.&lt;/strong&gt; A project is one HTML file. That is what makes every commit runnable, and it is the size of project this is for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declining a pull request from the target side.&lt;/strong&gt; A pull request is the proposer's node; the target's owner ignores it, or merges it. The proposer withdraws it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The application is four files, with no build step and nothing to deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;👉 &lt;strong&gt;&lt;a href="https://estebanrfp.github.io/dCode/" rel="noopener noreferrer"&gt;Open dCode&lt;/a&gt;&lt;/strong&gt; — sign in as &lt;code&gt;alice&lt;/code&gt; in one window and &lt;code&gt;bob&lt;/code&gt; in another, create a repository, type in both, commit, fork, propose, merge. Tick &lt;em&gt;Private&lt;/em&gt; on a repository and open it as a third identity to meet the locked door; the name at the top right opens your identity view, where a passkey can take a phrase session so it survives a reload.&lt;/li&gt;
&lt;li&gt;📄 &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/dCode" rel="noopener noreferrer"&gt;Read the source&lt;/a&gt;&lt;/strong&gt; — the model is documented in the README and in the constitution; the tests say what each claim rests on.&lt;/li&gt;
&lt;li&gt;📝 &lt;strong&gt;&lt;a href="https://genosdb.com/genosdb-collaborative-editing" rel="noopener noreferrer"&gt;Real-Time Collaborative Editing Without a CRDT&lt;/a&gt;&lt;/strong&gt; — the technique the editor is built on, with the fractional keys and the tie rule explained.&lt;/li&gt;
&lt;li&gt;🧭 See how GenosDB compares across &lt;a href="https://genosdb.com/popular-p2p-distributed-databases" rel="noopener noreferrer"&gt;other P2P and distributed databases&lt;/a&gt;, or read the &lt;a href="https://genosdb.com/gundb" rel="noopener noreferrer"&gt;full GunDB guide&lt;/a&gt; if that is where you are coming from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ If you like it, the best way to support the project is a star on GitHub — and a star is the one thing here no AI can give. Models can write code, review it and explain it; only a person can decide that a project deserves a star. If you are that person: &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/dCode" rel="noopener noreferrer"&gt;dCode&lt;/a&gt;&lt;/strong&gt; for the application and &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt;&lt;/strong&gt; for the engine that makes it possible. Or spin the engine up in seconds: &lt;code&gt;npm i genosdb&lt;/code&gt;.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This article is part of the official documentation of GenosDB (GDB).&lt;/strong&gt;&lt;br&gt;
GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by &lt;strong&gt;Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;estebanrfp&lt;/a&gt;)&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📄 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;Whitepaper&lt;/a&gt; | overview of GenosDB design and architecture&lt;/p&gt;

&lt;p&gt;🛠 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; | planned features and future updates&lt;/p&gt;

&lt;p&gt;💡 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; | code snippets and usage demos&lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/index.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | full reference guide&lt;/p&gt;

&lt;p&gt;🔍 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; | detailed API methods&lt;/p&gt;

&lt;p&gt;💬 &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; | community questions and feedback&lt;/p&gt;

&lt;p&gt;🗂 &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; | Minified production-ready files&lt;/p&gt;

&lt;p&gt;📦 &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;Install via npm&lt;/a&gt; | quick setup instructions&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://estebanrfp.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt; | &lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/estebanrfp/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>git</category>
      <category>p2p</category>
    </item>
    <item>
      <title>A Hacker News Clone With No Server — Four Files, a Constitution Every Peer Runs, Just GenosDB</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Sat, 05 Sep 2026 02:32:29 +0000</pubDate>
      <link>https://dev.to/estebanrfp/a-hacker-news-clone-with-no-server-four-files-a-constitution-every-peer-runs-just-genosdb-45je</link>
      <guid>https://dev.to/estebanrfp/a-hacker-news-clone-with-no-server-four-files-a-constitution-every-peer-runs-just-genosdb-45je</guid>
      <description>&lt;h2&gt;
  
  
  Every Hacker News Clone Ends With a Server
&lt;/h2&gt;

&lt;p&gt;The Hacker News clone is the "hello world" of web frameworks. There is one for Vue, one for React, one for Svelte, one for every backend-as-a-service — and they all end the same way: a server holds the stories, a database holds the votes, an auth service holds the accounts, and the front end asks them politely. The orange bar is the easy part.&lt;/p&gt;

&lt;p&gt;This one has no server. &lt;strong&gt;Four files, no build, no backend, no accounts.&lt;/strong&gt; Open the page and your browser holds the whole site — every story, comment, vote and flag — as a graph that syncs peer-to-peer over WebRTC with every other browser that has it open. Nobody hosts it. Nobody moderates it either: a &lt;strong&gt;constitution every peer runs&lt;/strong&gt; decides who may write, what a vote weighs, and when an item dies — and that constitution is a file in the repository. The engine underneath is &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt;, a graph database that lives in the browser, signs every write, and syncs between peers with nothing in the middle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqbg6drxlosuxthxrzooe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqbg6drxlosuxthxrzooe.png" alt="The front page of dNews: Hacker News's bar, ranks, points and threads, with no server behind it" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://estebanrfp.github.io/dNews/" rel="noopener noreferrer"&gt;dNews&lt;/a&gt; — sign in with one click, submit, vote; open a second browser and watch it follow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/dNews" rel="noopener noreferrer"&gt;estebanrfp/dNews&lt;/a&gt; — &lt;code&gt;index.html&lt;/code&gt;, &lt;code&gt;styles.css&lt;/code&gt;, &lt;code&gt;app.js&lt;/code&gt; (under 500 lines) and &lt;code&gt;constitution.js&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Whole Idea in Three Sentences
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The graph is the site, and every visitor holds it.&lt;/strong&gt; There is no API to call: &lt;code&gt;db.map&lt;/code&gt; is a live query over the nodes on your own disk, and the peers keep those nodes the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every node is signed and owned.&lt;/strong&gt; A story, a comment, a vote and a flag are nodes created by an identity; the engine refuses anyone else's edit or deletion, on every peer. "Who voted" is not a field you trust the site about — it is the owner nobody could forge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nobody decides; everybody calculates.&lt;/strong&gt; Points, karma, trust, ranking and the death of an item are derived by each browser from the same signed nodes, with the same rules. The only signature that grants anything is the authority's, and its only power is signing roles.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What a Shared Constitution Changes
&lt;/h2&gt;

&lt;p&gt;Every community site runs on rules. The difference is where they live. On a forum with moderators, the rules live in people: a ban is a judgement, a removed post is a mood, a shadowban is a decision nobody announced, and the same comment survives on Monday and dies on Friday depending on who is on shift. That is not a complaint about moderators; it is what any system built on human discretion does, and nobody can audit it from outside.&lt;/p&gt;

&lt;p&gt;A distributed site can put the rules somewhere else. Here they live in a file that every peer runs, and that changes five concrete things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Moderators&lt;/th&gt;
&lt;th&gt;A shared constitution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Who the rule applies to&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whoever the moderator notices&lt;/td&gt;
&lt;td&gt;Everyone, identically — the same code runs in every browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When you learn the rule&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;After it hits you&lt;/td&gt;
&lt;td&gt;Before you post: the constitution page is the file itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Can a decision be checked?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No — the log, if any, is theirs&lt;/td&gt;
&lt;td&gt;Yes — every flag and vote is a signed node; anyone recounts the verdict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Can content be removed quietly?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — nothing has a path to delete another's node; a killed item stays in grey, with its reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How rules change&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Silently, in a policy nobody diffs&lt;/td&gt;
&lt;td&gt;By pull request, in public, one diff at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things fall out of that which no amount of good will can give a moderated site. &lt;strong&gt;The rule cannot have a prejudice&lt;/strong&gt;, because it cannot see who you are — only what your nodes say and what trusted members did about them. And &lt;strong&gt;power is bounded by construction&lt;/strong&gt;: the one signature that grants anything belongs to the authority, and the engine gives it exactly one thing to sign, roles, by rules everyone can read. It cannot edit a post, delete a comment, or move a story down the page, because there is no operation for it. A system in which the moderator cannot abuse a power is different in kind from one in which the moderator promises not to.&lt;/p&gt;

&lt;p&gt;None of this makes a community wise — a bad rule, applied to everyone with perfect consistency, is still a bad rule. It makes it &lt;em&gt;legible&lt;/em&gt;: the rule is the argument, and the argument is a file you can fork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — One Subscription, Every Page
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gdb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dnews&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;superAdmins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;CONSTITUTION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authority&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;customRoles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CONSTITUTION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;governanceRules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;acls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// One live query feeds a store; the front page, a thread, a profile are&lt;/span&gt;
&lt;span class="c1"&gt;// pure functions of it. Nothing is fetched, nothing ticks over the wire.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$or&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;story&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flag&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vouch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;scheduleRender&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole data layer. &lt;code&gt;gdb(name, { rtc: true })&lt;/code&gt; opens a local database under that name and joins the room of the same name; the &lt;code&gt;sm&lt;/code&gt; block installs the constitution — the authority's address, the roles, the rules — and every peer must carry the same one, or their signatures mean different things. The callback fires once per node already on disk, then once per change from any peer; the &lt;code&gt;$or&lt;/code&gt; pulls the &lt;code&gt;user:&lt;/code&gt; nodes too, which is where the Security Manager keeps each identity's role.&lt;/p&gt;

&lt;p&gt;The front page is HN's own formula over that store — &lt;code&gt;(points − 1) / (hours + 2)^1.8&lt;/code&gt; — computed when you look at it. &lt;code&gt;newest&lt;/code&gt;, &lt;code&gt;past&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt;, &lt;code&gt;jobs&lt;/code&gt;, &lt;code&gt;threads&lt;/code&gt;: the same store, a different filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — A Vote Is a Node You Own
&lt;/h2&gt;

&lt;p&gt;On Hacker News a vote is a row in a table you cannot see. Here it is a node, and the node has an owner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No id: with `owner` on the value the engine names the node `${owner}:${uuid}`,&lt;/span&gt;
&lt;span class="c1"&gt;// and every peer refuses a write or a deletion from anyone else.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeWithPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// the engine's own verdict, before the write&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// the creator becomes the owner&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;comment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;story&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;storyId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db.sm.acls.set&lt;/code&gt; writes the node with the signer as &lt;code&gt;owner&lt;/code&gt;; the engine checks that ownership on every path a node can arrive through — live from a peer, or in the catch-up when you come back — so a modified client cannot cast a vote in your name, and neither can the authority. Taking a vote back is a write of &lt;code&gt;dir: 0&lt;/code&gt; on your own node, never a deletion; the site keeps no history, but it never rewrites one either.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0yqak1p2wn8ky0oc6eas.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0yqak1p2wn8ky0oc6eas.png" alt="A thread on dNews: comments are nodes owned by their authors, nested by a parent field" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — The Constitution Is a File
&lt;/h2&gt;

&lt;p&gt;Everything that decides who may do what is in &lt;a href="https://github.com/estebanrfp/dNews/blob/main/constitution.js" rel="noopener noreferrer"&gt;&lt;code&gt;constitution.js&lt;/code&gt;&lt;/a&gt;, and the site renders that file, verbatim, on its &lt;strong&gt;constitution&lt;/strong&gt; page. Two kinds of rule live in it, and the difference is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;guest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;can&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                                  &lt;span class="c1"&gt;// where everyone starts&lt;/span&gt;
  &lt;span class="nx"&gt;restricted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;can&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                                  &lt;span class="c1"&gt;// lost the right to write&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;can&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sync&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;    &lt;span class="c1"&gt;// posts, comments, votes, flags&lt;/span&gt;
  &lt;span class="nx"&gt;superadmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;can&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assignRole&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                &lt;span class="c1"&gt;// the authority: signs roles, nothing else&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="c1"&gt;// evaluated in order, the last match wins&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;offsetTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assignRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restricted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                       &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assignRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restricted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;karma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;assignRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;restricted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="nx"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;gravity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vouchKarma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vouchPenalty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flagKarma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flagsToKill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;downvoteKarma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deadScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Roles and rules are enforced by the engine.&lt;/strong&gt; A guest's write is refused on every receiver. A role is only valid with the authority's signature, and the governance engine assigns roles from these rules — last match wins, so demotion is just another rule: fall to −10 karma and the third rule catches you, climb back and the second one does. It runs on whichever device the authority is signed in on: whoever presses the one-click identity. The engine can also give that key to an always-on superpeer — a real option, and a possible next step for dNews; today it runs on browsers alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thresholds are derived by every peer.&lt;/strong&gt; Three flags from trusted members with enough karma kill an item; a killed item stays visible in grey, with the reason, to anyone who turns &lt;code&gt;showdead&lt;/code&gt; on. No moderator decided it, and anyone can recount it.&lt;/p&gt;

&lt;p&gt;To change a rule, open a pull request. The discussion is public, the diff is the amendment, and the site renders the file as merged.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nqomua2403mlfiqhb9i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nqomua2403mlfiqhb9i.png" alt="The constitution page: the same file the app runs, rendered — roles, rules, thresholds, and how to amend them" width="800" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Writing Is Free, Influence Is Earned
&lt;/h2&gt;

&lt;p&gt;A site where anyone can create an identity in a second has to answer one question: what stops a hundred fake accounts from voting a story to the top? Hacker News answers it with IP addresses and heuristics you cannot see. dNews answers it in the open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Trust is a chain of signed vouches that starts at the authority. A vote&lt;/span&gt;
&lt;span class="c1"&gt;// or a flag counts only from a trusted owner — a hundred fresh identities&lt;/span&gt;
&lt;span class="c1"&gt;// voting for each other add up to zero.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trusted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;AUTHORITY&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;grew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grew&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;grew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;vouches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trusted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;trusted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trusted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;grew&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countVote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trusted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;upKarma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;voter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;downvoteKarma&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone may post the moment the constitution makes them a user. But a vote or a flag &lt;em&gt;counts&lt;/em&gt; only if its owner is trusted, and trust is a chain of vouches: a trusted member with enough karma vouches for a newcomer by writing a node only they could have written. Every profile says who vouched for whom, and vouching costs — a voucher loses karma for every invitee that ends up restricted. Sybil-resistant by a public web of trust, not sybil-proof; nothing open is, and the site says so on its constitution page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Karma You Can Recount
&lt;/h2&gt;

&lt;p&gt;The role rules read &lt;code&gt;karma&lt;/code&gt; from each user's node, and a rule is only worth what the writer of its metric is worth — a number you write about yourself is self-service promotion. So the authority counts it: whoever holds that identity tallies the signed votes with the code above and writes the result on each user node, signed. Your own browser runs the same code over the same nodes, and the profile shows both numbers:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvi306ox3pbaq90fevoqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvi306ox3pbaq90fevoqh.png" alt="A profile on dNews: karma computed here beside the karma the authority certified, and who vouched for whom" width="798" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the two ever differ, the authority is behind or lying — and everyone can see which. That is the part no forum with moderators can offer: you can audit the moderator, because the moderator is a file and a signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does Not Do — and Why That Is the Design
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nobody edits or deletes anyone else's post.&lt;/strong&gt; Not a moderator, not the authority: the engine has no path for it. What the community can do is kill an item, in the open, by the rule above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two members writing the same node at once&lt;/strong&gt; never happens — every node has one owner. Two members voting at once are two nodes, and every peer counts both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mnemonic session lives in memory&lt;/strong&gt; and ends with the tab; a passkey keeps it on the device. Your identity is a key pair, and the phrase is the only way back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The engine holds one copy per visitor.&lt;/strong&gt; Close every browser that has the site open and the graph sleeps until one comes back. An always-on superpeer would keep it awake — the engine offers one, dNews does not use it yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/estebanrfp/dNews &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;dNews
bun tests/server.mjs        &lt;span class="c"&gt;# http://localhost:5705 — any static server will do&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing to build: the engine arrives from a CDN with one import. &lt;code&gt;?room=anything&lt;/code&gt; opens a private sandbox of the whole site; the tests use it, and so can you. Three &lt;a href="https://github.com/estebanrfp/dNews/blob/main/tests/dnews.spec.js" rel="noopener noreferrer"&gt;Playwright tests&lt;/a&gt; run one browser context per visitor over real WebRTC: a newcomer promoted by the constitution, a story and a thread crossing between peers, an unvouched vote that weighs nothing until someone vouches, and a device that comes back to find its graph on disk.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;examples catalogue&lt;/a&gt; has forty more pages built the same way, and the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;Security Manager guide&lt;/a&gt; and the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;governance guide&lt;/a&gt; cover every call used here.&lt;/p&gt;

&lt;p&gt;⭐ Found this useful? &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Star GenosDB on GitHub&lt;/a&gt;&lt;/strong&gt; — or spin it up in seconds: &lt;code&gt;npm i genosdb&lt;/code&gt;.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This article is part of the official documentation of GenosDB (GDB).&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by &lt;strong&gt;Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;estebanrfp&lt;/a&gt;)&lt;/strong&gt;. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📄 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;Whitepaper&lt;/a&gt; | overview of GenosDB design and architecture &lt;/p&gt;

&lt;p&gt;🛠 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; | planned features and future updates&lt;/p&gt;

&lt;p&gt;💡 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; | code snippets and usage demos&lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/index.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | full reference guide&lt;/p&gt;

&lt;p&gt;🔍 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; | detailed API methods&lt;/p&gt;

&lt;p&gt;📰 &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;Articles&lt;/a&gt; | concepts, tutorials and use cases&lt;/p&gt;

&lt;p&gt;💬 &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; | community questions and feedback&lt;/p&gt;

&lt;p&gt;🗂 &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; | Minified production-ready files&lt;/p&gt;

&lt;p&gt;📦 &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;Install via npm&lt;/a&gt; | quick setup instructions&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://estebanrfp.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt; | &lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/estebanrfp/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webrtc</category>
      <category>hackernews</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Stream Overlay You Control From Your Phone — No Server, One File in OBS, Just GenosDB</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Fri, 04 Sep 2026 20:05:08 +0000</pubDate>
      <link>https://dev.to/estebanrfp/a-stream-overlay-you-control-from-your-phone-no-server-one-file-in-obs-just-genosdb-b52</link>
      <guid>https://dev.to/estebanrfp/a-stream-overlay-you-control-from-your-phone-no-server-one-file-in-obs-just-genosdb-b52</guid>
      <description>&lt;h2&gt;
  
  
  The Usual Way Puts a Server Between You and OBS
&lt;/h2&gt;

&lt;p&gt;Every overlay tool works the same way. Cloud widgets — StreamElements, Streamlabs — keep the state on their servers and make you log in. The self-hosted ones run a Node or Python process, an MQTT broker or a WebSocket relay on your machine, and your phone talks to that. In every case there is a machine, an account, or both, between the hand holding the phone and the Browser Source in OBS.&lt;/p&gt;

&lt;p&gt;This tutorial removes the machine. &lt;strong&gt;One HTML file, no server, no accounts.&lt;/strong&gt; Add its URL to OBS as a Browser Source; open the same URL with &lt;code&gt;/control&lt;/code&gt; on your phone. The lower third, the ticker, the countdown and the alerts travel from the phone to OBS peer-to-peer, over WebRTC, through &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; — a graph database that lives in the browser and syncs between peers with nothing in the middle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgk4m8wspattq2v5pdrs2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgk4m8wspattq2v5pdrs2.png" alt="The overlay on a 1920×1080 stage — lower third, countdown, ticker and an alert — with the phone remote in front" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://estebanrfp.github.io/gdb/examples/obs-overlay.html" rel="noopener noreferrer"&gt;obs-overlay.html&lt;/a&gt; — name a stream, then open the two links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb/blob/main/examples/obs-overlay.html" rel="noopener noreferrer"&gt;one file on GitHub&lt;/a&gt;, about 430 lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Whole Idea in Three Sentences
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The name you give the stream is the link, and the link is the room.&lt;/strong&gt; &lt;code&gt;#friday-live&lt;/code&gt; is what OBS loads; &lt;code&gt;#friday-live/control&lt;/code&gt; is what your phone opens. Both join the database named after the stream, and that database is the room every peer syncs in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three pieces are three nodes.&lt;/strong&gt; The lower third, the ticker and the countdown each live in one node with a fixed id. Whoever holds the link can rewrite them, and every screen showing the stream repaints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts are moments, not state.&lt;/strong&gt; A "new follower" pops on screen for four seconds and is gone. It rides an ephemeral channel and is never stored — the one rule the GenosDB API reference insists on: persistent state in the graph, transient events on the channel.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1 — One File, Two Modes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gdb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.jsdelivr.net/npm/genosdb@latest/dist/index.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// "friday-live" · "control" or nothing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gdb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`overlay-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole backend. &lt;code&gt;gdb(name, { rtc: true })&lt;/code&gt; opens a local database under that name and joins the room of the same name; peers find each other through public relays that carry only the handshake, and the data itself goes &lt;strong&gt;directly between browsers over WebRTC&lt;/strong&gt;. The phone and OBS are two peers of that room. So is a second phone, if a co-host opens the same link.&lt;/p&gt;

&lt;p&gt;The rest of the file is an &lt;code&gt;if&lt;/code&gt;: with &lt;code&gt;mode === "control"&lt;/code&gt; the page draws the remote; without it, the overlay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Three Pieces, Three Nodes
&lt;/h2&gt;

&lt;p&gt;A fixed id per piece is safe here, because the database belongs to one stream. Writing is &lt;code&gt;db.put&lt;/code&gt; with that id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// { type: "lower-third", name, title, on }&lt;/span&gt;
&lt;span class="c1"&gt;// { type: "ticker",      text, on }&lt;/span&gt;
&lt;span class="c1"&gt;// { type: "timer",       label, endsAt, on }&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lower-third&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ticker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;write&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail is worth pausing on, because it is where the first bug lived. The patch lands on the local &lt;code&gt;state&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the put. Without that, two changes made in the same instant — a name typed, then the switch pressed — compose badly: the second one reads a state that is still a frame behind the first, and overwrites it. Typing, in the file, updates &lt;code&gt;state&lt;/code&gt; on every keystroke and only writes after a 250 ms pause; a switch pressed mid-typing flushes the text with it. One put per pause, never one per key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — One Subscription Paints Every Screen
&lt;/h2&gt;

&lt;p&gt;The overlay and every control panel share the same code for reading: one live query over the three types.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lower-third&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ticker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="nf"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// the overlay slides the lower third in; the remote mirrors the switches&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db.map&lt;/code&gt; with a callback fires once per existing node, then once per change — from this page, from the phone, from a second phone — through the same callback. There is no "send to OBS" step and no polling: OBS's page holds a live query, and the phone's put is what satisfies it.&lt;/p&gt;

&lt;p&gt;Because the remote reads through the same subscription, two phones agree with each other: the switch you press shows as &lt;em&gt;Showing&lt;/em&gt; on the co-host's phone the moment it lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — A Countdown Nobody Ticks
&lt;/h2&gt;

&lt;p&gt;The naive countdown sends "11:59", "11:58" … from the phone. This one sends nothing. The phone stores an &lt;strong&gt;end time&lt;/strong&gt;, and every screen derives the remaining seconds on its own clock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;endsAt&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="c1"&gt;// on the phone&lt;/span&gt;
&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timer-start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;timer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;endsAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One node, written once. OBS, a second OBS, and both phones show the same second, and the room carries no traffic while the timer runs. If a clock is off by a second, that screen is off by a second — which is the honest cost of having no server clock, and the same cost every distributed timer pays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Alerts Are Moments, Not State
&lt;/h2&gt;

&lt;p&gt;An alert is the opposite kind of thing. Nobody needs to know, an hour later, that ada_lovelace followed at 18:04:12. So it never touches the graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alerts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// phone&lt;/span&gt;
&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New follower: ada_lovelace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// overlay&lt;/span&gt;
&lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alert&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;// pops in, stays 4 s, fades — pure CSS&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;
  &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alerts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animationend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db.room.channel(name)&lt;/code&gt; is GenosDB's ephemeral lane: a named data channel to every peer in the room, no persistence, no catch-up. An overlay that joins later never sees old alerts, and that is correct — it also never sees old cursor positions in a collaborative editor, which is the same idea. The rule from the API reference is one question: &lt;em&gt;does this need to survive a reload?&lt;/em&gt; The lower third does. An alert does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Put It in OBS
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In OBS, add a &lt;strong&gt;Browser&lt;/strong&gt; source to your scene.&lt;/li&gt;
&lt;li&gt;URL: the overlay link (the one without &lt;code&gt;/control&lt;/code&gt;). Width 1920, height 1080.&lt;/li&gt;
&lt;li&gt;Open the control link on your phone. Type a name, press &lt;em&gt;Showing&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;OBS's Browser Source is Chromium, and it injects a &lt;code&gt;window.obsstudio&lt;/code&gt; object into every page it loads. The file uses that to decide what to paint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;obsstudio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// an ordinary browser: a dark stage with the two links&lt;/span&gt;
  &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stage-hint&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside OBS the body stays transparent and only the pieces paint. Anywhere else, the same URL shows a dark stage with the overlay on it and the control link underneath, so the file explains itself when someone opens it by hand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jwp9ua3mjmzlulb02j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46jwp9ua3mjmzlulb02j.png" alt="The remote on a phone: what is on air above, the controls below" width="780" height="2480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does Not Do — and What It Would Take
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Whoever has the link controls the overlay.&lt;/strong&gt; For a stream that is exactly right — you, a co-host, a moderator you trust — and the link is not something you post in chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two phones editing the same field at the same moment:&lt;/strong&gt; both edits survive when they touch different parts of it; only the same words at the same instant are the later writer's, deterministically on every peer. Different pieces never collide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No history, no undo&lt;/strong&gt;, by design: three nodes hold the present, and alerts hold nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that is one decision — &lt;em&gt;no identity&lt;/em&gt; — and GenosDB has the layer for the day the overlay needs one: a Security Manager that gives each phone a cryptographic identity, signs every operation, and lets a node carry an &lt;code&gt;owner&lt;/code&gt; so only its author, and whoever they grant, can change it — enforced by every peer, not by a server. The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;Security Manager guide&lt;/a&gt; covers it; for an overlay it is a configuration option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run It
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://estebanrfp.github.io/gdb/examples/obs-overlay.html" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;, name a stream, and paste the overlay link into OBS. Or save &lt;a href="https://github.com/estebanrfp/gdb/blob/main/examples/obs-overlay.html" rel="noopener noreferrer"&gt;the file&lt;/a&gt; and serve it from anywhere static; the only dependency is one import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gdb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.jsdelivr.net/npm/genosdb@latest/dist/index.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;examples catalogue&lt;/a&gt; has forty more pages built the same way — the &lt;a href="https://estebanrfp.github.io/gdb/examples/thermostat.html" rel="noopener noreferrer"&gt;thermostat&lt;/a&gt; is this overlay's smaller sibling, one tab commanding another — and the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; covers every call used here.&lt;/p&gt;

&lt;p&gt;⭐ Found this useful? &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Star GenosDB on GitHub&lt;/a&gt;&lt;/strong&gt; — or spin it up in seconds: &lt;code&gt;npm i genosdb&lt;/code&gt;.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This article is part of the official documentation of GenosDB (GDB).&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by &lt;strong&gt;Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;estebanrfp&lt;/a&gt;)&lt;/strong&gt;. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📄 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;Whitepaper&lt;/a&gt; | overview of GenosDB design and architecture &lt;/p&gt;

&lt;p&gt;🛠 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; | planned features and future updates&lt;/p&gt;

&lt;p&gt;💡 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; | code snippets and usage demos&lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/index.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | full reference guide&lt;/p&gt;

&lt;p&gt;🔍 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; | detailed API methods&lt;/p&gt;

&lt;p&gt;📰 &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;Articles&lt;/a&gt; | concepts, tutorials and use cases&lt;/p&gt;

&lt;p&gt;💬 &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; | community questions and feedback&lt;/p&gt;

&lt;p&gt;🗂 &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; | Minified production-ready files&lt;/p&gt;

&lt;p&gt;📦 &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;Install via npm&lt;/a&gt; | quick setup instructions&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://estebanrfp.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt; | &lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/estebanrfp/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webrtc</category>
      <category>obs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a Splitwise-Style Expense Splitter With No Backend — 500 Lines, No Accounts, Just GenosDB</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Fri, 04 Sep 2026 20:04:58 +0000</pubDate>
      <link>https://dev.to/estebanrfp/build-a-splitwise-style-expense-splitter-with-no-backend-500-lines-no-accounts-just-genosdb-1eh7</link>
      <guid>https://dev.to/estebanrfp/build-a-splitwise-style-expense-splitter-with-no-backend-500-lines-no-accounts-just-genosdb-1eh7</guid>
      <description>&lt;h2&gt;
  
  
  What the "Clone" Guides Don't Tell You
&lt;/h2&gt;

&lt;p&gt;Search for &lt;em&gt;how to build an app like Splitwise&lt;/em&gt; and you get two kinds of answers. Agencies quote &lt;strong&gt;12 to 24 weeks and a five-figure budget&lt;/strong&gt;. GitHub clones — MERN, Appwrite, Firebase — hand you a server, a database, an auth flow and a deployment before a single expense is split.&lt;/p&gt;

&lt;p&gt;Both are answering a question you may not have asked. An expense splitter is a small problem: a few people, a list of who paid what, and one number per person. The size comes from the infrastructure, not from the problem.&lt;/p&gt;

&lt;p&gt;This tutorial builds the whole thing in &lt;strong&gt;one HTML file, about 500 lines, with no server and no accounts&lt;/strong&gt;. Two phones open the same link and see the same balances, live. Here is the finished app with a four-person trip loaded:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1kv1zigrq1ugyjkhemi6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1kv1zigrq1ugyjkhemi6.png" alt="Split expenses — a four-person trip: people, balances, who pays whom, and the expense list" width="800" height="1423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://estebanrfp.github.io/gdb/examples/split-expenses.html" rel="noopener noreferrer"&gt;split-expenses.html&lt;/a&gt; — open it in two windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb/blob/main/examples/split-expenses.html" rel="noopener noreferrer"&gt;one file on GitHub&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It runs on &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt;, a graph database that lives in the browser and syncs peer-to-peer over WebRTC. Everything below is plain JavaScript against its API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Whole Idea in Three Sentences
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The group is the link.&lt;/strong&gt; The name you give the group becomes the URL, and the URL is the room every device syncs in. Whoever holds the link is in — there is no invitation table because there is no table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An expense is a node.&lt;/strong&gt; Who paid, how much, split among whom. That is &lt;em&gt;everything&lt;/em&gt; the app stores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A balance is never stored.&lt;/strong&gt; It is derived, on every change, from the whole set of expenses — which is what makes the sync visible: two windows either show the same balances or you can see they have not converged yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else is HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — The Group Is the Link
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gdb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.jsdelivr.net/npm/genosdb@latest/dist/index.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// The link carries the group's name; the name is the database name; the&lt;/span&gt;
&lt;span class="c1"&gt;// database name is the room every peer syncs in.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gdb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`split-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire backend. &lt;code&gt;gdb(name, { rtc: true })&lt;/code&gt; opens a local database under that name — persisted in the browser's own storage — and joins the room of the same name. Peers discover each other through public relays that carry nothing but the handshake; the data itself travels &lt;strong&gt;directly between browsers over WebRTC&lt;/strong&gt;. Close every tab and the data is still on each device; open one again and it syncs with whoever is there.&lt;/p&gt;

&lt;p&gt;When the page opens without a hash, it shows a single field: &lt;em&gt;Name your group&lt;/em&gt;. Submitting it writes the name into the hash and reloads. Sharing the link is the invitation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — People and Expenses Are Nodes
&lt;/h2&gt;

&lt;p&gt;A person is a node. An expense is a node. Both are written with &lt;code&gt;db.put&lt;/code&gt;, which returns the node's id:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A person&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// An expense: who paid, how much (integer cents), split among whom&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expense&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dinner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aliceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aliceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bobId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;carolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;danaId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Amounts are &lt;strong&gt;integer cents&lt;/strong&gt;, never floats. That is not pedantry: every peer will compute the balances on its own, and &lt;code&gt;0.1 + 0.2&lt;/code&gt; is how two devices end up disagreeing by a cent forever.&lt;/p&gt;

&lt;p&gt;Nothing else is written. There is no &lt;code&gt;balances&lt;/code&gt; collection, no &lt;code&gt;group&lt;/code&gt; document holding totals, no counter to keep in step. If you have ever debugged a cached total that drifted from its source, you know why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Two Live Queries Feed One View
&lt;/h2&gt;

&lt;p&gt;The page subscribes once to each kind of node, and lets the engine sort:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;member&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;renderMembers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;renderBalances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expense&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;rowOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildExpenseRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;initial&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// arrives already newest-first&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;added&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// newest by definition&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;rowOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;replaceWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;// updated: rebuilt in place&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;renderBalances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db.map&lt;/code&gt; with a callback is a live query. It fires once per existing node (&lt;code&gt;initial&lt;/code&gt;), then once per change — &lt;code&gt;added&lt;/code&gt;, &lt;code&gt;updated&lt;/code&gt;, &lt;code&gt;removed&lt;/code&gt; — whether the change came from this window, another tab, or another device. There is no polling, no "refresh" button, and no second code path for remote changes: a peer's expense arrives through exactly the same callback as your own.&lt;/p&gt;

&lt;p&gt;Two things fall out of this for free. A device that was offline writes locally and syncs when it is back — the callback fires on the other devices as if the expense had just been added. And a device that opens the link for the first time receives the whole set from whoever is online, one &lt;code&gt;initial&lt;/code&gt; event per node.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — A Balance Is a Sum Over Everything That Arrived
&lt;/h2&gt;

&lt;p&gt;This is the heart of the app, and it is fourteen lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
    &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;share&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;rest&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you paid, minus your share of everything you took part in, over &lt;strong&gt;every&lt;/strong&gt; expense. It runs on every change, and it costs nothing at the scale of a group.&lt;/p&gt;

&lt;p&gt;Two details make it converge everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The split rounds down and hands the leftover cents to the first participants in id order.&lt;/strong&gt; 10.00 among three is 3.34, 3.33, 3.33 — and &lt;em&gt;which&lt;/em&gt; person gets the extra cent is decided by sorting ids, so every device decides the same.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The balances of a group therefore sum to exactly zero&lt;/strong&gt;, always. If your app ever shows a group that does not, it is not a rounding bug: it is two peers that have not converged yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second point is the reason to build the app this way. A stored total can be wrong quietly. A derived total is wrong &lt;em&gt;visibly&lt;/em&gt; — and only until the last expense arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Who Pays Whom
&lt;/h2&gt;

&lt;p&gt;Settling up is the classic greedy pass: the largest debtor pays the largest creditor, repeat. It produces at most &lt;em&gt;n − 1&lt;/em&gt; transfers, and ties break on id so every peer lists the same ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;settleUp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byOwed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;debtors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byOwed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;creditors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;byOwed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transfers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;debtors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;creditors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debtors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;creditors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;transfers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;debtors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;creditors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;debtors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creditors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;cents&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;transfers&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when someone actually pays? &lt;strong&gt;A settlement is one more expense&lt;/strong&gt; — paid by the debtor, split among exactly one person, the creditor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;expense&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Settlement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;split&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No special case, no second node type, no "mark as settled" flag to keep in sync. The derived balances close by themselves, and the transfer disappears from the list on every device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — Open It in Two Windows
&lt;/h2&gt;

&lt;p&gt;Add three people in one window. Add a dinner paid by Alice. The other window shows &lt;code&gt;Alice +20.00 · Bob −10.00 · Carol −10.00&lt;/code&gt; the moment the node lands. Add a taxi from the second window, split between two — both windows now agree on &lt;code&gt;+15.50 · −5.50 · −10.00&lt;/code&gt; and on the same two transfers. Mark one as paid, and it is gone from both.&lt;/p&gt;

&lt;p&gt;Nothing in the page compares states or reconciles totals. It cannot: there is no total to reconcile. Each window derives its numbers from the expenses it holds, and the expenses are what the database keeps identical. When the sets are equal, the numbers are equal, and you can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does Not Do — and What It Would Take
&lt;/h2&gt;

&lt;p&gt;This is a tutorial, and it is honest about its scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Anyone with the link can add, edit and delete anything.&lt;/strong&gt; That is the Splitwise-with-friends model, and it is fine for a trip. It is not fine for strangers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two people editing the same expense at the same moment&lt;/strong&gt;: both edits survive when they change different things — the amount and who paid, say; only the same field at the same instant is the later writer's, on every peer. Different expenses never collide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The group's name is the link&lt;/strong&gt;, so a guessable name is a guessable group.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are the same decision — &lt;em&gt;no identity&lt;/em&gt; — and GenosDB has the layer for the day you want it: a &lt;strong&gt;Security Manager&lt;/strong&gt; that gives each device a cryptographic identity (a passkey, or a recovery phrase), signs every operation, and lets a node carry an &lt;code&gt;owner&lt;/code&gt; so that only its author — and whoever they grant — can change it, enforced by every peer rather than by a server. Adding it to this app is a configuration option and one field per node; the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;Security Manager guide&lt;/a&gt; and the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/zero-trust-security-model.md" rel="noopener noreferrer"&gt;zero-trust model&lt;/a&gt; cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run It
&lt;/h2&gt;

&lt;p&gt;Open the &lt;a href="https://estebanrfp.github.io/gdb/examples/split-expenses.html" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; in two windows, or save &lt;a href="https://github.com/estebanrfp/gdb/blob/main/examples/split-expenses.html" rel="noopener noreferrer"&gt;the file&lt;/a&gt; and open it from any static host. The only dependency is one import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gdb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cdn.jsdelivr.net/npm/genosdb@latest/dist/index.min.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;examples catalogue&lt;/a&gt; has forty more pages built the same way, and the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; covers every call used here.&lt;/p&gt;

&lt;p&gt;⭐ Found this useful? &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Star GenosDB on GitHub&lt;/a&gt;&lt;/strong&gt; — or spin it up in seconds: &lt;code&gt;npm i genosdb&lt;/code&gt;.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This article is part of the official documentation of GenosDB (GDB).&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by &lt;strong&gt;Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;estebanrfp&lt;/a&gt;)&lt;/strong&gt;. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📄 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;Whitepaper&lt;/a&gt; | overview of GenosDB design and architecture &lt;/p&gt;

&lt;p&gt;🛠 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; | planned features and future updates&lt;/p&gt;

&lt;p&gt;💡 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; | code snippets and usage demos&lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/index.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | full reference guide&lt;/p&gt;

&lt;p&gt;🔍 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; | detailed API methods&lt;/p&gt;

&lt;p&gt;📰 &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;Articles&lt;/a&gt; | concepts, tutorials and use cases&lt;/p&gt;

&lt;p&gt;💬 &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; | community questions and feedback&lt;/p&gt;

&lt;p&gt;🗂 &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; | Minified production-ready files&lt;/p&gt;

&lt;p&gt;📦 &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;Install via npm&lt;/a&gt; | quick setup instructions&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://estebanrfp.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt; | &lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/estebanrfp/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webrtc</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>GenosDB vs Yjs and Automerge: Real-Time Collaboration Without a CRDT Library</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Mon, 31 Aug 2026 17:58:45 +0000</pubDate>
      <link>https://dev.to/estebanrfp/genosdb-vs-yjs-and-automerge-real-time-collaboration-without-a-crdt-library-4ffg</link>
      <guid>https://dev.to/estebanrfp/genosdb-vs-yjs-and-automerge-real-time-collaboration-without-a-crdt-library-4ffg</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Everyone Solves the Same Way
&lt;/h2&gt;

&lt;p&gt;Two people open the same document. Both type. Nobody loses a word.&lt;/p&gt;

&lt;p&gt;The standard answer to that requirement is a &lt;strong&gt;CRDT library&lt;/strong&gt; — usually &lt;a href="https://yjs.dev" rel="noopener noreferrer"&gt;Yjs&lt;/a&gt; or &lt;a href="https://automerge.org" rel="noopener noreferrer"&gt;Automerge&lt;/a&gt;. Both are excellent, and both solve the hardest part of the problem: merging concurrent edits without a coordinator. But a CRDT library is a merge engine, not an application backend. Once it is in place, the rest of the system is still missing — and most of what is missing is what your app actually spends its code on.&lt;/p&gt;

&lt;p&gt;This article looks at what each library does, what it leaves for you to build, and how the same live experience can be reached with a database that already carries the missing pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Yjs Actually Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yjs&lt;/strong&gt; is a shared-types library built on &lt;strong&gt;YATA&lt;/strong&gt;, a sequence CRDT. Its data structures — &lt;code&gt;Y.Text&lt;/code&gt;, &lt;code&gt;Y.Array&lt;/code&gt;, &lt;code&gt;Y.Map&lt;/code&gt; — merge concurrent operations deterministically, so every peer converges on the same document regardless of arrival order.&lt;/p&gt;

&lt;p&gt;Its strengths are real and worth naming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Character-level merge.&lt;/strong&gt; Two cursors inside the same sentence interleave correctly. This is the hard case, and Yjs handles it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State vectors.&lt;/strong&gt; A peer describes what it has as &lt;code&gt;{clientID → clock}&lt;/code&gt;, and the other side computes an exact delta. Efficient when writers are few and stable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editor bindings.&lt;/strong&gt; Adapters exist for ProseMirror, CodeMirror, Monaco, Quill and TipTap — separate packages, each on its own release cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure JavaScript&lt;/strong&gt;, with tombstone garbage collection and &lt;code&gt;Y.mergeUpdates&lt;/code&gt; for compacting history without instantiating a document. A Rust port (&lt;code&gt;yrs&lt;/code&gt;) exists for native peers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What Yjs is not, by design, is a stack. It is a merge engine, and everything around it is a separate decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transport.&lt;/strong&gt; The public demos run against a &lt;strong&gt;y-websocket&lt;/strong&gt; server. There are WebRTC providers, but the well-trodden path — the one every tutorial takes — is a server relaying updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence.&lt;/strong&gt; &lt;code&gt;y-indexeddb&lt;/code&gt; is a separate package, wired by you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity, permissions, queries.&lt;/strong&gt; Not in scope. A Yjs document has no concept of who wrote what, no roles, and nothing to query — it is a document, not a database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One detail is worth pulling out, because it shapes everything below: &lt;strong&gt;awareness — the cursors, selections, names and colours you see moving on screen — is not CRDT data even in Yjs.&lt;/strong&gt; It travels on a separate, ephemeral protocol and dies with the session. Yjs made that call deliberately: presence is not history, so it should not pay the price of history.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Automerge Actually Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automerge&lt;/strong&gt; takes a different route: an operation-based CRDT with &lt;strong&gt;full history&lt;/strong&gt;. Every change is a node in a hash-addressed DAG — the same idea as a Git commit graph — so a document carries its own past, and time travel and per-character attribution come for free.&lt;/p&gt;

&lt;p&gt;Its strengths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Character-level merge&lt;/strong&gt;, like Yjs, plus rich text with marks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History as a first-class citizen.&lt;/strong&gt; Every version is reachable and every change is attributable, without you designing a versioning scheme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficient sync protocol.&lt;/strong&gt; Peers exchange &lt;em&gt;heads&lt;/em&gt; plus Bloom filters; identical heads mean nothing is transferred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform core&lt;/strong&gt; in Rust, compiled to WASM for the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And its costs, which are the flip side of the same design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A document never forgets.&lt;/strong&gt; History is the data model, so opening a long-lived document means loading its past, and the cost grows with the document's lifetime rather than with its current size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WASM in the bundle.&lt;/strong&gt; Fine for most apps, awkward for some environments and for cold-start budgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access control is a separate layer.&lt;/strong&gt; &lt;a href="https://www.inkandswitch.com/keyhive/" rel="noopener noreferrer"&gt;Keyhive&lt;/a&gt; is the answer being built for it, and it is explicitly experimental.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A sync server in the standard setup.&lt;/strong&gt; &lt;code&gt;automerge-repo&lt;/code&gt; ships one, and the documented path uses it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Dividing Line, Stated Plainly
&lt;/h2&gt;

&lt;p&gt;There is exactly one thing these libraries do that a plain database does not: &lt;strong&gt;interleave two edits inside the same span of text, at the same instant, character by character.&lt;/strong&gt; It is a narrower case than it sounds. Two people typing into the same paragraph at once keep both edits when they touch different places: the store keeps the winner, and the writer whose write lost the race re-applies its own edit over it as an ordinary signed write — no CRDT, no merged value that nobody signed (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-concurrent-writes.md" rel="noopener noreferrer"&gt;how it works&lt;/a&gt;). Only the same span at the same instant is the later writer's, with both carets in view — and an application that needs even that can add it itself, keystroke by keystroke on the ephemeral channel, where it needs it rather than everywhere. The &lt;a href="https://estebanrfp.github.io/gdb/examples/keystrokes.html" rel="noopener noreferrer"&gt;Keystrokes example&lt;/a&gt; does exactly that: one paragraph everyone types into at once, inside the same word if you like, with the channel and the graph shown side by side as they happen.&lt;/p&gt;

&lt;p&gt;That capability is genuinely hard to build, and it is the reason both projects exist. It is also, in the collaborative applications most teams actually ship, the rarest case in the room — and the one that human beings avoid on their own the moment they can see where everyone else is standing. That is the observation Notion, Linear and every block editor since have been built on: &lt;strong&gt;make presence visible and collisions mostly stop happening.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything else people reach for a CRDT to get — concurrent edits across a document, ordering that survives simultaneous inserts, offline writes that reconcile on reconnect, live cursors — does not require a sequence CRDT at all. It requires the right granularity and a presence channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Other Path: One Node per Paragraph
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GenosDB&lt;/strong&gt; is a peer-to-peer graph database, not an editor library. But because a document can be modelled &lt;em&gt;as data&lt;/em&gt;, collaborative editing falls out of the database's own primitives. The &lt;a href="https://estebanrfp.github.io/gdb/examples/block-editor.html" rel="noopener noreferrer"&gt;live example&lt;/a&gt; is a continuous document — write, press Enter, keep writing — implemented in one HTML file with no editor framework and no CRDT dependency. The technique has four parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A paragraph is a node.&lt;/strong&gt; Editing a paragraph rewrites only that node, so two people working on different paragraphs never overwrite each other — there is no shared node to fight over. Last-write-wins is a problem only when two writers share a node, and at paragraph granularity they usually do not — and when they do, both edits survive unless they touch the same span.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fractional order keys.&lt;/strong&gt; A paragraph inserted between order &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; gets a key in the gap. Two peers inserting into the same gap concurrently mint different keys, so &lt;strong&gt;both survive&lt;/strong&gt;, and every peer sorts them identically — an engine guarantee, see &lt;em&gt;Ordering, Precisely&lt;/em&gt; below. No counters to coordinate, no rebalancing to synchronise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure is graph operations.&lt;/strong&gt; Enter splits a paragraph into two nodes; Backspace at the start merges a paragraph into the previous one and deletes it; pasting multi-line text creates one node per line. Concurrent structural edits land on different nodes, so they compose instead of conflicting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Awareness on an ephemeral channel.&lt;/strong&gt; Named, coloured carets and live selections travel over a GenosRTC data channel and never touch the graph — the same architectural decision Yjs made, reached from the same reasoning. And the paragraph being typed is broadcast keystroke by keystroke, so remote windows move &lt;strong&gt;character by character&lt;/strong&gt;, while the debounced database write remains the truth that persists and repairs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result behaves like the CRDT demos: type in one window, watch it appear letter by letter in the other, with a named cursor showing where your collaborator is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F973nzwyyursnto98h1b7.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F973nzwyyursnto98h1b7.webp" alt="Several peers editing the same document: each cursor carries its peer's name and colour, and selections are visible across windows" width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every window is a separate peer. The coloured cursors and selections travel on the ephemeral channel; the text itself lives in the graph.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Ordering, Precisely
&lt;/h2&gt;

&lt;p&gt;The second bullet above makes a promise that deserves a mechanism: &lt;em&gt;every peer sorts them identically&lt;/em&gt;. Here is exactly what holds it up, and where it stops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A key is a double.&lt;/strong&gt; A paragraph inserted between order &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;2&lt;/code&gt; gets a key at a random point in the gap — random so that two peers inserting into the same gap at the same instant mint different keys, and both survive. A float64 carries 53 bits of mantissa, and each insert into the &lt;em&gt;same&lt;/em&gt; gap keeps about half of what is left: after roughly fifty successive inserts between the same two paragraphs, the next key rounds onto its neighbour and the two tie. Typing a document top to bottom never gets there, because every new paragraph opens a fresh gap of one. A multi-line paste is the one action that would, so it mints its keys in a single batch: the gap is divided once, into one slot per line, and each line lands inside its own slot. Two hundred pasted lines spend about eight bits of the mantissa instead of exhausting all fifty-three.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frln0jjilut7862c96uan.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frln0jjilut7862c96uan.png" alt="One gap between two paragraphs, split two ways: one key per insert halves what is left until a key rounds onto its neighbour; a paste divides the gap once, into one slot per line" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Same gap, same scale. Inserting one key at a time halves what is left until a key lands on the neighbour; a paste divides the gap once and never gets close.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The example draws this live. Beside the document, every paragraph's key sits on a number line, to scale: the four warm dots are a paste that just landed, spread once across one gap, and the panel says how many halvings the gap under the caret has left.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwo98x0maoynx5fb49cht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwo98x0maoynx5fb49cht.png" alt="One peer's window of the block editor: the document on the left, and on the right every paragraph's order key on a number line. Four warm dots are the paste that just landed, spread once across one gap; the highlighted gap under the caret reads 47 halvings left; the named caret in the last paragraph is the other peer." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One peer's window. The other peer's named caret sits in the last paragraph; on the right, the keys the paragraphs are sorted by, the paste's four dots still warm and the gap under the caret with 47 halvings left.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The engine settles ties.&lt;/strong&gt; A sorted &lt;code&gt;db.map&lt;/code&gt; breaks a tie on the node id, in the direction of the sort. Two replicas holding the same nodes read &lt;code&gt;results&lt;/code&gt;, the &lt;code&gt;initial&lt;/code&gt; events and every &lt;code&gt;$limit&lt;/code&gt;/&lt;code&gt;$after&lt;/code&gt; page in the same order, whatever order their nodes arrived in, and &lt;code&gt;desc&lt;/code&gt; is the exact reverse of &lt;code&gt;asc&lt;/code&gt;. Before, a tie fell to arrival order, which differs on every peer, and the examples had to compensate by hand. The rule costs one comparison per tie and thirty-nine bytes gzipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the key lives is the whole difference.&lt;/strong&gt; &lt;a href="https://www.bartoszsypytkowski.com/scaling-fractional-indexes/" rel="noopener noreferrer"&gt;Bartosz Sypytkowski&lt;/a&gt;, who maintains Yrs — Yjs in Rust — arrived at the same trade from inside the CRDT world. Scaling fractional indexes to a million spreadsheet rows, he sets interleaving aside as a cost the granularity absorbs, and spends his design on packing each key into eight bytes and on a move semantics of tombstones and pointers. He needs that machinery because in his model the key &lt;em&gt;is&lt;/em&gt; the row's identity: to move a row you delete it and insert another, and two concurrent moves leave two rows unless a move CRDT reconciles them. In GenosDB the node id is the identity and &lt;code&gt;order&lt;/code&gt; is a field it carries. Moving a paragraph is one &lt;code&gt;put&lt;/code&gt;; two peers moving the same paragraph at once resolve, by last-write-wins on the hybrid logical clock, to one paragraph in one place — no tombstone, no pointer, no second key space.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12nzsk6tkdu04ohkydhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F12nzsk6tkdu04ohkydhr.png" alt="Where the key lives: in a fractional-index CRDT the key names the row, so a move is a delete plus an insert; in GenosDB the id names the paragraph and order is a value, so a move is one put" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In a fractional-index CRDT the key names the row, so a move is a delete plus an insert. In GenosDB the id names the paragraph and position is a value it carries, so a move is the write it already knows how to do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The same technique, taken to code, is the subject of the next article: &lt;a href="https://genosdb.com/dcode-p2p-collaborative-code-editor-no-server" rel="noopener noreferrer"&gt;A GitHub With No Server&lt;/a&gt; applies one-node-per-line editing to a code editor and adds what GitHub runs servers for — owned commits, branch protection and pull requests — from the same graph.&lt;/p&gt;

&lt;p&gt;That is also the honest boundary. There are no string keys, no rebalancing and no move CRDT here, by design: a document is hundreds of blocks, not a million rows, and the float, the batch and the tie rule cover that range with nothing to maintain. Should a table with a million rows ever be the target, his post is the map. The pattern, its limit and the engine's rule are written up in the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/ordered-lists.md" rel="noopener noreferrer"&gt;Ordered Lists guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The same keys now run a second example, a &lt;a href="https://estebanrfp.github.io/gdb/examples/spreadsheet.html" rel="noopener noreferrer"&gt;spreadsheet&lt;/a&gt;: rows and columns are identities, position is a key, and a cell is a node named after its row and its column — so a row moves with its cells in one &lt;code&gt;put&lt;/code&gt;, and a column inserted in the middle shifts the letters, not the data. It also answers the figure that opens his post: Alice and Bob typing into &lt;code&gt;A3&lt;/code&gt; of an empty sheet while apart, and ending with six rows and two cells. Here the base grid is never written. A row nobody inserted has the same id and the same key on every peer, so both write the same cell and end with one value — his virtual key, at no cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Path Is Shorter
&lt;/h2&gt;

&lt;p&gt;Both approaches produce live text on a remote screen. The difference is what a single keystroke has to do to get there.&lt;/p&gt;

&lt;p&gt;In the standard CRDT setup, a keystroke is encoded into one or more CRDT operations, sent to a &lt;strong&gt;sync server&lt;/strong&gt;, relayed to each peer, integrated into the receiving replica's structure, and rendered back through an editor binding. Every step is fast; there are simply several of them, and one of them is a round trip through infrastructure you have to run.&lt;/p&gt;

&lt;p&gt;In the block-editor example, a keystroke is &lt;strong&gt;one hop over a direct WebRTC data channel&lt;/strong&gt; — coalesced to at most one message per animation frame — and the receiver assigns a string to a native &lt;code&gt;textarea&lt;/code&gt;. No encoding step, no integration step, no binding, no server. The graph write that persists it happens behind the live view, debounced, and repairs anything the network dropped.&lt;/p&gt;

&lt;p&gt;That is an architectural difference, not a benchmark: fewer stages and no relay hop. It is also why the demo is a single file you can save and open, with nothing to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes in the Same Package
&lt;/h2&gt;

&lt;p&gt;This is where the comparison stops being about merge algorithms. A CRDT library gives you a document. GenosDB gives you a database that happens to be able to hold one — so the surrounding application does not need a second stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queries.&lt;/strong&gt; &lt;code&gt;db.map({ query, field, order, $limit })&lt;/code&gt; — filters, ordering, pagination, full-text matching and recursive graph traversal with &lt;code&gt;$edge&lt;/code&gt;. A document is a query result, not a special case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity.&lt;/strong&gt; Cryptographic, sovereign identities with mnemonic recovery and &lt;strong&gt;WebAuthn passkeys&lt;/strong&gt; built in. Every operation can be signed by its author.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions.&lt;/strong&gt; Role-based access control and node-level ACLs, enforced independently by &lt;strong&gt;every peer&lt;/strong&gt; against the verified signer — a zero-trust model, not a client-side courtesy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption.&lt;/strong&gt; Field-level and record-level, with cryptographic read revocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence.&lt;/strong&gt; OPFS-backed, offline by default. No adapter to choose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport.&lt;/strong&gt; WebRTC peer-to-peer with automatic peer discovery and a &lt;strong&gt;Cellular Mesh&lt;/strong&gt; overlay that keeps connection counts flat as rooms grow, instead of the O(n²) full mesh. No sync server anywhere in the picture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is bolted on for the editor: it is the same engine every other GenosDB application uses. The editor is roughly two hundred lines on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline Is Not a Feature You Add
&lt;/h2&gt;

&lt;p&gt;The claim worth testing in a peer-to-peer system is not what happens while everyone is connected — it is what happens when they are not.&lt;/p&gt;

&lt;p&gt;Run the example across multiple devices, disconnect the network entirely, write a different paragraph on each one, and reconnect. Every document converges, no matter how many peers wrote offline or in which order — because the graph is stored locally, the operation log carries what each peer missed, and a state digest lets converged peers exchange nothing at all. The live cursors come back on their own.&lt;/p&gt;

&lt;p&gt;That behaviour is not editor code. It is the database doing what it does for every application built on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Choice Actually Is
&lt;/h2&gt;

&lt;p&gt;The choice is not between three merge algorithms. It is between a library that owns your document and a database that holds it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A sequence CRDT — Yjs, Automerge — owns the document.&lt;/strong&gt; It interleaves keystrokes inside the same words, and in Automerge's case keeps the whole history. It does so blindly: after hours apart, two versions of a paragraph are woven into a text nobody wrote, with no conflict to show anyone. It comes as a library with its own data model, metadata that grows with every character ever typed, and a sync server or a second transport beside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GenosDB holds the document as data.&lt;/strong&gt; Two people on the same paragraph keep both edits when they touch different places; the same span at the same instant is the later writer's, in view, never woven; and an application that needs keystroke-level interleaving adds it where it needs it, on the ephemeral channel, instead of paying for it everywhere. Queries, identity, permissions, encryption, offline persistence and peer-to-peer transport come with the same nodes, and every value is one writer's, signed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are not mutually exclusive, either. Because GenosDB stores arbitrary values, binary CRDT updates can travel as ordinary nodes in the graph — the transport, persistence, identity and access control come from the database, and the merge engine stays whatever you chose. The point is not that CRDTs are unnecessary. It is that &lt;strong&gt;most collaborative applications reach for one to get things a database should already give them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The example is a single HTML file, with no build step and nothing to deploy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;👉 &lt;strong&gt;&lt;a href="https://estebanrfp.github.io/gdb/examples/block-editor.html" rel="noopener noreferrer"&gt;Open the Block Editor&lt;/a&gt;&lt;/strong&gt; — then open it again in a second window and type in both.&lt;/li&gt;
&lt;li&gt;📄 &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb/blob/main/examples/block-editor.html" rel="noopener noreferrer"&gt;Read the source&lt;/a&gt;&lt;/strong&gt; — the technique is documented in comments, in place.&lt;/li&gt;
&lt;li&gt;⌨️ &lt;strong&gt;&lt;a href="https://estebanrfp.github.io/gdb/examples/keystrokes.html" rel="noopener noreferrer"&gt;Open Keystrokes&lt;/a&gt;&lt;/strong&gt; — the narrow case, added where an app wants it: one paragraph everyone types into at once; on the right, the channel keystroke by keystroke, the graph snapshot by snapshot, and a hash proving every window shows the same text.&lt;/li&gt;
&lt;li&gt;🧮 &lt;strong&gt;&lt;a href="https://estebanrfp.github.io/gdb/examples/spreadsheet.html" rel="noopener noreferrer"&gt;Open the Spreadsheet&lt;/a&gt;&lt;/strong&gt; — the same keys in two dimensions; type into &lt;code&gt;A3&lt;/code&gt; in both windows and watch one cell, not two rows.&lt;/li&gt;
&lt;li&gt;🌿 &lt;strong&gt;&lt;a href="https://estebanrfp.github.io/dCode/" rel="noopener noreferrer"&gt;Open dCode&lt;/a&gt;&lt;/strong&gt; — the block editor's technique on a code editor: one node per line, named carets, the page running beside the code as you type, and underneath it a code host with branches, forks and pull requests, every commit signed, owned and runnable. No server. The sequel to this article, &lt;a href="https://genosdb.com/dcode-p2p-collaborative-code-editor-no-server" rel="noopener noreferrer"&gt;A GitHub With No Server&lt;/a&gt;, explains how.&lt;/li&gt;
&lt;li&gt;🧭 See how GenosDB compares across &lt;a href="https://genosdb.com/popular-p2p-distributed-databases" rel="noopener noreferrer"&gt;other P2P and distributed databases&lt;/a&gt;, or read the &lt;a href="https://genosdb.com/gundb" rel="noopener noreferrer"&gt;full GunDB guide&lt;/a&gt; if that is where you are coming from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⭐ Found this useful? &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Star GenosDB on GitHub&lt;/a&gt;&lt;/strong&gt; — or spin it up in seconds: &lt;code&gt;npm i genosdb&lt;/code&gt;.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This article is part of the official documentation of GenosDB (GDB).&lt;/strong&gt;&lt;br&gt;
GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by &lt;strong&gt;Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;estebanrfp&lt;/a&gt;)&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📄 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;Whitepaper&lt;/a&gt; | overview of GenosDB design and architecture&lt;/p&gt;

&lt;p&gt;🛠 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; | planned features and future updates&lt;/p&gt;

&lt;p&gt;💡 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; | code snippets and usage demos&lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/index.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | full reference guide&lt;/p&gt;

&lt;p&gt;🔍 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; | detailed API methods&lt;/p&gt;

&lt;p&gt;💬 &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; | community questions and feedback&lt;/p&gt;

&lt;p&gt;🗂 &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; | Minified production-ready files&lt;/p&gt;

&lt;p&gt;📦 &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;Install via npm&lt;/a&gt; | quick setup instructions&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://estebanrfp.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt; | &lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/estebanrfp/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>crdt</category>
      <category>javascript</category>
      <category>webrtc</category>
      <category>database</category>
    </item>
    <item>
      <title>GenosDB + GenosRTC: A Scalable Browser-Native P2P Graph Database with Cellular Mesh WebRTC Architecture</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:22:34 +0000</pubDate>
      <link>https://dev.to/estebanrfp/genosdb-genosrtc-a-scalable-browser-native-p2p-graph-database-with-cellular-mesh-webrtc-2hhb</link>
      <guid>https://dev.to/estebanrfp/genosdb-genosrtc-a-scalable-browser-native-p2p-graph-database-with-cellular-mesh-webrtc-2hhb</guid>
      <description>&lt;p&gt;When comparing &lt;strong&gt;GenosDB + GenosRTC&lt;/strong&gt; against the current browser-P2P ecosystem — Gun, PeerJS, simple-peer, Trystero, y-webrtc — it is important to distinguish between two very different things: &lt;strong&gt;exceptional execution of existing technologies&lt;/strong&gt; and &lt;strong&gt;genuine architectural innovation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GenosDB delivers both. This article breaks down exactly where each one lives, with architecture diagrams and links to the full technical documentation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 &lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;genosdb&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🗂 &lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;github.com/estebanrfp/gdb&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;Documentation index:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/index.md" rel="noopener noreferrer"&gt;docs/index.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📄 &lt;strong&gt;Whitepaper:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;WHITEPAPER.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;Wiki:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb/wiki" rel="noopener noreferrer"&gt;GitHub Wiki&lt;/a&gt; · &lt;a href="https://deepwiki.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;DeepWiki&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is GenosDB?&lt;/li&gt;
&lt;li&gt;The Full Stack at a Glance&lt;/li&gt;
&lt;li&gt;Layer 1 — Production-Grade P2P Signaling (GenosRTC)&lt;/li&gt;
&lt;li&gt;Layer 2 — The Real Innovation: Cellular Mesh&lt;/li&gt;
&lt;li&gt;Layer 3 — The Database Engine: Delta Sync, HLC, Last-Write-Wins&lt;/li&gt;
&lt;li&gt;Layer 4 — Zero-Trust Security: RBAC, ACLs, Governance&lt;/li&gt;
&lt;li&gt;Market Comparison&lt;/li&gt;
&lt;li&gt;Why the Combination Is the Moat&lt;/li&gt;
&lt;li&gt;Getting Started in 30 Seconds&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;Documentation Map&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What is GenosDB?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB (GDB)&lt;/a&gt; is a &lt;strong&gt;distributed, modular, peer-to-peer graph database&lt;/strong&gt; that runs entirely in the browser — no servers, no backend, no infrastructure. It combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;graph data model&lt;/strong&gt; (nodes + directed edges) with a rich query engine — see the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;GDB API Reference&lt;/a&gt; and &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-features.md" rel="noopener noreferrer"&gt;Features overview&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time P2P synchronization&lt;/strong&gt; over WebRTC via &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-api-reference.md" rel="noopener noreferrer"&gt;GenosRTC&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/zero-trust-security-model.md" rel="noopener noreferrer"&gt;Zero-Trust Security Model&lt;/a&gt;&lt;/strong&gt; with cryptographic identities, WebAuthn, RBAC, node-level ACLs, and rule-based governance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime dependencies&lt;/strong&gt;, with geospatial &lt;code&gt;$near&lt;/code&gt; queries built into the engine
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Web App
    │
    │ await gdb("my-db", { rtc: true })
    ▼
GenosDB
│
├─► Query Engine      (20+ operators, $edge, $near)
│
├─► Security Manager  (RBAC · ACLs · WebAuthn)
│
└─► Sync Core         (HLC · oplog · LWW)
     │
     ├─► Persistence Worker  (OPFS → IndexedDB)
     │
     └─► GenosRTC            (WebRTC + Nostr + Cells)
          │
          ▼
     Other Peers (P2P)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Full Stack at a Glance
&lt;/h2&gt;

&lt;p&gt;The platform is built as four independent layers. The first one is &lt;em&gt;excellent engineering on known ideas&lt;/em&gt;. The other three are where the architectural differentiation lives.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Status vs. market&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signaling &amp;amp; transport&lt;/td&gt;
&lt;td&gt;WebRTC + Nostr relays&lt;/td&gt;
&lt;td&gt;Production-grade execution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network topology&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cellular Mesh overlay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No browser-native equivalent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data consistency&lt;/td&gt;
&lt;td&gt;HLC + oplog delta sync + per-node last-write-wins, and concurrent writes to one node keep both&lt;/td&gt;
&lt;td&gt;Rare in browser P2P&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Zero-trust RBAC + ACLs + signed governance&lt;/td&gt;
&lt;td&gt;Unique vertical integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Layer 1 — Production-Grade P2P Signaling (GenosRTC)
&lt;/h2&gt;

&lt;p&gt;Decentralized WebRTC signaling over &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-architecture.md" rel="noopener noreferrer"&gt;Nostr&lt;/a&gt; is not new. &lt;strong&gt;The implementation details are what matter&lt;/strong&gt; — and GenosRTC's details are production-grade (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-architecture.md" rel="noopener noreferrer"&gt;full architecture breakdown&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pool of 5 pre-generated WebRTC offers&lt;/strong&gt; for near-instant connection establishment — no offer-generation latency on peer discovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent relay management&lt;/strong&gt;: primary relays plus adaptive fallback strategies with dynamic scheduling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic detection and exclusion of relays demanding Proof-of-Work&lt;/strong&gt;, keeping signaling latency predictable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connectivity-aware reconnection&lt;/strong&gt; driven by browser &lt;code&gt;online&lt;/code&gt;/&lt;code&gt;visibility&lt;/code&gt; events with exponential backoff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AES-GCM SDP encryption&lt;/strong&gt; with &lt;code&gt;SHA-256(secret:appId:roomId)&lt;/code&gt; key derivation — signaling metadata is never readable by relays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16KB chunked transfers&lt;/strong&gt; with binary metadata and per-chunk progress tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audio/video streaming&lt;/strong&gt; with track replacement and per-stream metadata (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-guide.md" rel="noopener noreferrer"&gt;GenosRTC guide&lt;/a&gt;)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nostr Relays
    │
    │ peer discovery only
    ▼
Peer A announces topic
    │
    ▼
Peer B discovers Peer A
    │
    ├─► OFFER    (pooled, AES-GCM SDP)
    │
    └─► ANSWER   (encrypted)
         │
         ▼
Direct WebRTC channel   (E2E, no relays)
│
├─► named channels
│
├─► 16KB chunks + progress
│
└─► audio/video streams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Libraries such as PeerJS or simple-peer do not even address the signaling problem — they assume you run your own signaling server. Finding &lt;strong&gt;all&lt;/strong&gt; of these concerns solved together, in a dependency-free package, is rare.&lt;/p&gt;

&lt;p&gt;However, the true differentiation is &lt;strong&gt;not&lt;/strong&gt; the signaling layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2 — The Real Innovation: Cellular Mesh
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-cells.md" rel="noopener noreferrer"&gt;Cellular Mesh&lt;/a&gt; was designed to solve the fundamental scalability problem of every traditional WebRTC full-mesh architecture: O(N²) connection growth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Solutions such as Trystero, PeerJS, Gun, and y-webrtc all ultimately rely on topologies that become increasingly inefficient as peer counts grow. In practice, these architectures hit serious performance and stability limitations once networks reach &lt;strong&gt;dozens of simultaneous participants&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FULL MESH (market standard)
    │
    │ N·(N-1)/2 connections
    ▼
  8 peers  →    28 links
 50 peers  → 1,225 links
100 peers  → 4,950 links  💥

CELLULAR MESH (GenosRTC)
    │
    │ ~O(N) connections
    ▼
Peers
│
├─► Cell A   ● ● ● ◉
│
├─► Cell B   ● ● ● ◉
│
└─► Cell C   ● ● ● ◉

◉ bridge peer   (deterministically elected)
│
└─► forwards to neighbor cells
     (cell → bridge → cell, TTL-bounded)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GenosRTC addresses this with a &lt;strong&gt;self-organizing Cellular Mesh overlay&lt;/strong&gt; that currently has &lt;strong&gt;no direct equivalent in the browser-P2P market&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic cell assignment&lt;/strong&gt; — nodes are mapped to cells with no coordinator and no negotiation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Epoch-sealed topology&lt;/strong&gt; — the whole layout derives from a sealed roster snapshot; a room of N peers forms &lt;code&gt;ceil(N / cellSize)&lt;/code&gt; cells (one knob, default 10), and no topology data ever travels the wire&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic bridge election with succession&lt;/strong&gt; — one bridge per cell, chosen by a per-cell hash of the sealed roster, with no coordinator and no negotiation; the rank order doubles as a succession line, so a dead bridge is replaced at the next seal without a single message&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total cell isolation&lt;/strong&gt; — enforced at three independent layers, so inter-cell data flows exclusively through elected bridges:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cell isolation  (three independent layers)
│
├─► connections   exist only along sealed roles
│
├─► sending       every frame audience-targeted at the source
│
└─► receiving     a role guard drops anything else
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic TTL&lt;/strong&gt; — message propagation uses &lt;code&gt;TTL = min(150, ceil(log2(cells + 1)) * 2 + 3)&lt;/code&gt;, so flood depth adapts to the actual topology instead of being a magic number&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ring-buffer deduplication&lt;/strong&gt; — duplicate suppression over the last 5,000 message IDs, preventing routing loops at near-zero memory cost&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bye beacons and census-driven membership&lt;/strong&gt; — a departing peer clears itself from the census in one relay hop; cleanup follows the sealed roster, and topology changes collapse into atomic epoch seals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a &lt;strong&gt;structured, self-organizing overlay network running entirely on pure browser WebRTC&lt;/strong&gt;. A live visualization is available in the repository: &lt;a href="https://github.com/estebanrfp/gdb/blob/main/examples/mesh-cells-monitor.html" rel="noopener noreferrer"&gt;the mesh monitor&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Conceptually, the closest comparison is &lt;strong&gt;libp2p + gossipsub&lt;/strong&gt;. But that approach requires substantially more infrastructure, a significantly heavier stack, and far greater operational complexity. No existing browser-native solution provides a comparable architecture as a lightweight, transparent overlay enabled with a single flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gdb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To the best of our knowledge, &lt;strong&gt;Cellular Mesh is not an evolution of an existing browser-P2P pattern — it is a fundamentally different networking model&lt;/strong&gt;, and today it remains unique to GenosRTC.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3 — The Database Engine: Delta Sync, HLC, Last-Write-Wins
&lt;/h2&gt;

&lt;p&gt;A scalable transport is useless without a consistency model that survives it. GenosDB pairs Cellular Mesh with a sync engine designed for hostile network conditions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid Delta Protocol
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-hybrid-delta-protocol.md" rel="noopener noreferrer"&gt;Hybrid Delta Protocol&lt;/a&gt; is a dual-mode engine: &lt;strong&gt;delta updates&lt;/strong&gt; for speed, &lt;strong&gt;full-state fallback&lt;/strong&gt; for reliability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Peer reconnects (watermark T)
    │
    ▼
does the oplog still cover T?
│
├─► YES → DELTA SYNC
│    │
│    └─► only ops newer than T
│        (MessagePack + compressed)
│
└─► NO → FULL-STATE SYNC
     │
     ├─► compressed graph snapshot
     │
     └─► reset oplog + watermark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Hybrid Logical Clocks + LWW
&lt;/h3&gt;

&lt;p&gt;Every operation carries an &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-hybrid-logical-clock.md" rel="noopener noreferrer"&gt;HLC timestamp&lt;/a&gt; &lt;code&gt;{physical, logical}&lt;/code&gt; guaranteeing &lt;strong&gt;causal ordering without trusting wall clocks&lt;/strong&gt;. Conflicts resolve deterministically via Last-Write-Wins, with clock-skew capping so a peer with a broken clock cannot poison the graph. Two peers writing the same node at the same instant, each over the same value, keep both contributions: the store keeps the winner, and the peer whose write lost re-applies its own edit over it as an ordinary signed write — fields from whoever changed them, the one region each side changed in a string, the winner's for the same span (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-concurrent-writes.md" rel="noopener noreferrer"&gt;how it works&lt;/a&gt;). No CRDT and no metadata persisted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-Blocking Persistence
&lt;/h3&gt;

&lt;p&gt;A dedicated &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-worker-architecture.md" rel="noopener noreferrer"&gt;persistence worker&lt;/a&gt; writes through a tiered storage strategy — synchronous OPFS → async OPFS → IndexedDB — with cross-tab safety via the Web Locks API. Large graph saves never block the UI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query Engine
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/map-guide.md" rel="noopener noreferrer"&gt;map() query engine&lt;/a&gt; supports 20+ operators — comparison, logical, text/regex/like, &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/cursor-based-pagination.md" rel="noopener noreferrer"&gt;cursor-based pagination&lt;/a&gt; (&lt;code&gt;$after&lt;/code&gt; / &lt;code&gt;$before&lt;/code&gt; / &lt;code&gt;$limit&lt;/code&gt;), and the recursive &lt;strong&gt;&lt;code&gt;$edge&lt;/code&gt; graph-traversal operator&lt;/strong&gt; — all with real-time subscriptions. Full CRUD docs: &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/put-guide.md" rel="noopener noreferrer"&gt;put&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/get-guide.md" rel="noopener noreferrer"&gt;get&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/link-guide.md" rel="noopener noreferrer"&gt;link&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/remove-guide.md" rel="noopener noreferrer"&gt;remove&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/crud-operations-guide.md" rel="noopener noreferrer"&gt;overview&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 4 — Zero-Trust Security: RBAC, ACLs, Governance
&lt;/h2&gt;

&lt;p&gt;This is where vertical integration becomes a moat. In a P2P network &lt;strong&gt;you cannot trust any peer&lt;/strong&gt; — so GenosDB assumes none of them (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-distributed-trust-model.md" rel="noopener noreferrer"&gt;Distributed Trust Model&lt;/a&gt;, &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/zero-trust-security-model.md" rel="noopener noreferrer"&gt;Zero-Trust Security Model&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;op arrives from the network
    │
    │ every peer verifies it
    ▼
verify ECDSA signature      ✗ reject
    │ (recover the signer)
    ▼
check RBAC role + expiry    ✗ reject
    │ (guest → superadmin)
    ▼
check node-level ACLs       ✗ reject
    │ (verified signer only)
    ▼
apply to graph
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographic identity&lt;/strong&gt;: every user is an ECDSA keypair, protected by &lt;strong&gt;WebAuthn passkeys&lt;/strong&gt; or recoverable via &lt;strong&gt;BIP39 mnemonic&lt;/strong&gt; (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;SM API Reference&lt;/a&gt;, &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-architecture.md" rel="noopener noreferrer"&gt;SM Architecture&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hierarchical RBAC&lt;/strong&gt; with role expiry — &lt;code&gt;guest → user → manager → admin → superadmin&lt;/code&gt;, every grant signed by an authorized signer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-level ACLs&lt;/strong&gt; — owner/collaborator permissions per node, &lt;strong&gt;enforced against malicious peers&lt;/strong&gt;: the permission check uses the cryptographically verified signer, never a claimed identity (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;ACLs Module&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule-based governance&lt;/strong&gt; — automatic role promotion/demotion where rule conditions are native GenosDB queries, evaluated last-match-wins and signed by an online superadmin (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;Governance Guide&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every operation can be cryptographically validated. Every identity can be independently verified. Every node enforces ACL policy &lt;strong&gt;without centralized infrastructure and without assuming any other peer is trustworthy&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Market Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;GenosDB + GenosRTC&lt;/th&gt;
&lt;th&gt;Gun&lt;/th&gt;
&lt;th&gt;Trystero&lt;/th&gt;
&lt;th&gt;PeerJS / simple-peer&lt;/th&gt;
&lt;th&gt;y-webrtc&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serverless signaling&lt;/td&gt;
&lt;td&gt;✅ Nostr&lt;/td&gt;
&lt;td&gt;⚠️ needs relays&lt;/td&gt;
&lt;td&gt;✅ Nostr et al.&lt;/td&gt;
&lt;td&gt;❌ own server&lt;/td&gt;
&lt;td&gt;⚠️ signaling server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalable topology (&amp;gt;100 peers)&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Cellular Mesh&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌ full mesh&lt;/td&gt;
&lt;td&gt;❌ full mesh&lt;/td&gt;
&lt;td&gt;❌ full mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph data model + queries&lt;/td&gt;
&lt;td&gt;✅ 20+ operators, &lt;code&gt;$edge&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;⚠️ graph, limited queries&lt;/td&gt;
&lt;td&gt;❌ transport only&lt;/td&gt;
&lt;td&gt;❌ transport only&lt;/td&gt;
&lt;td&gt;❌ (Yjs CRDT docs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delta sync + full-state fallback&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ Yjs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Causal ordering (HLC)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ vector-ish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-trust RBAC + role expiry&lt;/td&gt;
&lt;td&gt;✅ signed, enforced&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node-level ACLs vs malicious peers&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ SEA (different model)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule-based governance&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime dependencies&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;several&lt;/td&gt;
&lt;td&gt;minimal&lt;/td&gt;
&lt;td&gt;several&lt;/td&gt;
&lt;td&gt;Yjs stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A note on Gun: its SEA layer provides cryptographic identity and encryption, but it has &lt;strong&gt;no equivalent to a zero-trust RBAC model with signed role grants, role expiry, rule-based governance, and node-level ACLs that remain enforced in the presence of malicious peers&lt;/strong&gt; across the network.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Combination Is the Moat
&lt;/h2&gt;

&lt;p&gt;GenosDB's competitive advantage is not WebRTC itself. It is not Nostr. It is not decentralized signaling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The defensible innovation is Cellular Mesh and its native integration with GenosDB's consistency, synchronization, and security model:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cellular Mesh         (scale)
    +
Hybrid Delta + HLC    (consistency)
    +
Signed RBAC + ACLs    (trust)
    │
    ▼
a fully distributed, zero-trust,
browser-native data platform —
beyond the full-mesh ceiling,
with no servers at all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer alone is valuable. Together they represent a &lt;strong&gt;unique architectural approach to browser-native distributed systems&lt;/strong&gt; — one that, as of today, has no direct equivalent in the browser-P2P market.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started in 30 Seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;gdb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;// P2P database with cellular mesh, no servers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;gdb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;my-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Write&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;builder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Real-time reactive query&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;unsubscribe&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;builder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;realtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Graph edges&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;📦 Install: &lt;code&gt;npm install genosdb&lt;/code&gt; — &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/bundler-configuration.md" rel="noopener noreferrer"&gt;bundler configuration&lt;/a&gt; for Vite, Webpack, Bun, esbuild, and CDN&lt;/li&gt;
&lt;li&gt;🧪 50+ live examples: &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;examples catalog&lt;/a&gt; — chat, kanban, whiteboard, collaborative editors, video rooms, geolocation&lt;/li&gt;
&lt;li&gt;🧯 Optional high-availability superpeer: &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-fallback-server.md" rel="noopener noreferrer"&gt;Fallback Server&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is GenosDB really serverless?&lt;/strong&gt;&lt;br&gt;
Yes. Peer discovery uses public Nostr relays (or &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-fallback-server.md" rel="noopener noreferrer"&gt;your own&lt;/a&gt;); all data flows peer-to-peer over encrypted WebRTC channels. An optional &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-fallback-server.md" rel="noopener noreferrer"&gt;fallback superpeer&lt;/a&gt; can improve availability, but nothing requires it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many peers can a room handle?&lt;/strong&gt;&lt;br&gt;
Standard mesh is comfortable for small rooms; with &lt;code&gt;{ cells: true }&lt;/code&gt; the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-cells.md" rel="noopener noreferrer"&gt;Cellular Mesh overlay&lt;/a&gt; replaces O(N²) connection growth with cell-local connectivity plus health-elected bridges, designed to scale to very large peer counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when a peer goes offline and comes back?&lt;/strong&gt;&lt;br&gt;
The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-hybrid-delta-protocol.md" rel="noopener noreferrer"&gt;Hybrid Delta Protocol&lt;/a&gt; sends only the missed operations; if the peer is too far behind, it transparently falls back to a compressed full-state snapshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How are conflicts resolved?&lt;/strong&gt;&lt;br&gt;
Deterministic Last-Write-Wins ordered by &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-hybrid-logical-clock.md" rel="noopener noreferrer"&gt;Hybrid Logical Clocks&lt;/a&gt;, with clock-skew capping — and when two peers write the same node at the same instant, over the same value, both contributions are kept rather than one lost (&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-concurrent-writes.md" rel="noopener noreferrer"&gt;the loser's rescue&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can untrusted users join?&lt;/strong&gt;&lt;br&gt;
That is the default assumption. The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/zero-trust-security-model.md" rel="noopener noreferrer"&gt;Zero-Trust Security Model&lt;/a&gt; starts everyone as a write-blocked guest; promotion happens through signed &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;role grants&lt;/a&gt; or automatic &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;governance rules&lt;/a&gt;, and &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;node-level ACLs&lt;/a&gt; hold even against malicious peers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work offline?&lt;/strong&gt;&lt;br&gt;
Yes — local-first by design. Data persists in OPFS/IndexedDB via a &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-worker-architecture.md" rel="noopener noreferrer"&gt;non-blocking worker&lt;/a&gt; and syncs when connectivity returns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Documentation Map
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Links&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core API&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-features.md" rel="noopener noreferrer"&gt;Features&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/crud-operations-guide.md" rel="noopener noreferrer"&gt;CRUD&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/put-guide.md" rel="noopener noreferrer"&gt;put&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/get-guide.md" rel="noopener noreferrer"&gt;get&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/map-guide.md" rel="noopener noreferrer"&gt;map&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/link-guide.md" rel="noopener noreferrer"&gt;link&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/remove-guide.md" rel="noopener noreferrer"&gt;remove&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/cursor-based-pagination.md" rel="noopener noreferrer"&gt;pagination&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GenosRTC&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-architecture.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-guide.md" rel="noopener noreferrer"&gt;Guide&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-cells.md" rel="noopener noreferrer"&gt;Cellular Mesh&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-fallback-server.md" rel="noopener noreferrer"&gt;Nostr relay deployment&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/zero-trust-security-model.md" rel="noopener noreferrer"&gt;Zero-Trust Model&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-distributed-trust-model.md" rel="noopener noreferrer"&gt;Distributed Trust&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;SM API&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-architecture.md" rel="noopener noreferrer"&gt;SM Architecture&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;ACLs&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;Governance&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internals&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-worker-architecture.md" rel="noopener noreferrer"&gt;Worker Architecture&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-hybrid-delta-protocol.md" rel="noopener noreferrer"&gt;Hybrid Delta Protocol&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-hybrid-logical-clock.md" rel="noopener noreferrer"&gt;Hybrid Logical Clock&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-fallback-server.md" rel="noopener noreferrer"&gt;Fallback Server&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/estebanrfp/gdb#readme" rel="noopener noreferrer"&gt;README&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;Whitepaper&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-resources.md" rel="noopener noreferrer"&gt;Resources&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/wiki" rel="noopener noreferrer"&gt;Wiki&lt;/a&gt; · &lt;a href="https://deepwiki.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;DeepWiki&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;npm&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keywords:&lt;/strong&gt; GenosDB, GenosRTC, P2P database, WebRTC mesh, decentralized database, browser database, Nostr signaling, CRDT alternative, distributed systems, zero-trust security, real-time sync, serverless, local-first, offline-first, graph database, JavaScript P2P&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Author
&lt;/h2&gt;

&lt;p&gt;Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;@estebanrfp&lt;/a&gt;) — Full Stack JavaScript Developer&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://estebanrfp.com/" rel="noopener noreferrer"&gt;estebanrfp.com&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/estebanrfp/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · Creator of &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>javascript</category>
      <category>database</category>
      <category>architecture</category>
    </item>
    <item>
      <title>A Live WebGPU Planet With No Backend — Powered by GenosDB</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:22:33 +0000</pubDate>
      <link>https://dev.to/estebanrfp/a-live-webgpu-planet-with-no-backend-powered-by-genosdb-4mnj</link>
      <guid>https://dev.to/estebanrfp/a-live-webgpu-planet-with-no-backend-powered-by-genosdb-4mnj</guid>
      <description>&lt;p&gt;Most database demos are to-do lists. I wanted to find out what happens when you&lt;br&gt;
put one under &lt;em&gt;real&lt;/em&gt; load — so I built a planet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OVGrid&lt;/strong&gt; is a cube-sphere planet you explore as an avatar, right in your&lt;br&gt;
browser: procedural terrain, weather, vegetation, day/night, real-time&lt;br&gt;
multiplayer presence, and land you can own and trade on-chain. It runs at&lt;br&gt;
real scale on &lt;strong&gt;WebGPU&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;▶ Play it live (no install, no account): &lt;a href="https://world.ovgrid.com" rel="noopener noreferrer"&gt;https://world.ovgrid.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But the rendering isn't the interesting part. The interesting part is that&lt;br&gt;
&lt;strong&gt;there is no backend.&lt;/strong&gt; No game server, no API, no socket server. A single&lt;br&gt;
distributed database — &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; — drives the&lt;br&gt;
entire live state of the world. This post is about how that works.&lt;/p&gt;


&lt;h2&gt;
  
  
  First, the planet: Floating Origin
&lt;/h2&gt;

&lt;p&gt;A planet is 795 km in radius. GPUs work in 32-bit floats, which run out of&lt;br&gt;
precision long before you get that big — geometry jitters, z-fighting explodes,&lt;br&gt;
the avatar shakes. The fix is &lt;strong&gt;Floating Origin&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The CPU holds every world coordinate in &lt;strong&gt;Float64&lt;/strong&gt; (full precision).&lt;/li&gt;
&lt;li&gt;Each frame, positions are rebased &lt;strong&gt;relative to the camera&lt;/strong&gt; and only the
small camera-relative deltas are handed to the GPU as &lt;strong&gt;Float32&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The camera is effectively always at the origin, so the numbers the GPU sees stay&lt;br&gt;
tiny and precise — jitter-free all the way down to street-level LOD. A dynamic&lt;br&gt;
quadtree on the cube-sphere subdivides terrain near you, and a GPU-baked height&lt;br&gt;
grid gives exact ground for collision and physics. (Full write-up:&lt;br&gt;
&lt;a href="https://github.com/estebanrfp/ovgrid/blob/main/docs/floating-origin-guide.md" rel="noopener noreferrer"&gt;floating-origin-guide.md&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;That's the foundation. Now the twist.&lt;/p&gt;


&lt;h2&gt;
  
  
  The twist: a database is the backend
&lt;/h2&gt;

&lt;p&gt;Live multiplayer worlds usually need a server: something authoritative that&lt;br&gt;
clients connect to, that holds state and relays updates. OVGrid has none. Instead,&lt;br&gt;
every client runs &lt;strong&gt;GenosDB&lt;/strong&gt;, a serverless, peer-to-peer, real-time graph&lt;br&gt;
database, and the world &lt;em&gt;is&lt;/em&gt; the database.&lt;/p&gt;

&lt;p&gt;Here's the capability map:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you see in the world&lt;/th&gt;
&lt;th&gt;What GenosDB is doing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Other players appear as animated avatars at the right position, altitude and animation&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;P2P presence&lt;/strong&gt; over WebRTC (&lt;code&gt;GenosRTC&lt;/code&gt;), synced ~12 times a second — no game server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The land map and the marketplace update live as data changes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Reactive reads&lt;/strong&gt; (&lt;code&gt;db.map&lt;/code&gt;) — the UI subscribes to a query and re-renders on every change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Land and assets you own, read instantly even offline&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;On-chain mirror&lt;/strong&gt; — Polygon is the source of truth, GenosDB caches it for fast, offline, reactive reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign in with biometrics or a seed phrase; actions are signed&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Decentralized identity + RBAC&lt;/strong&gt; via the Security Manager (WebAuthn / BIP39)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Reactive reads: the UI never polls
&lt;/h3&gt;

&lt;p&gt;The land map and the on-chain marketplace are &lt;em&gt;different frontends&lt;/em&gt; that read the&lt;br&gt;
&lt;em&gt;same database the same way&lt;/em&gt; — a live query with a callback that fires on every&lt;br&gt;
change, whether the change came from this peer or another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Subscribe once. The callback re-fires whenever matching data changes —&lt;/span&gt;
&lt;span class="c1"&gt;// locally OR from a remote peer. No polling, no manual refresh.&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;parcel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderLandMap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// re-render the 3D world on each change&lt;/span&gt;
  &lt;span class="c1"&gt;// ...and the marketplace subscribes to the very same data&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Broadcasting your avatar is just a write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Position, yaw and animation, ~12 Hz. Peers receive it and animate your avatar.&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;presence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;yaw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anim&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;`presence_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;myId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The pattern I'm proudest of: the read layer never changes
&lt;/h3&gt;

&lt;p&gt;A parcel of land might be &lt;strong&gt;claimed locally&lt;/strong&gt; (instant, optimistic) or &lt;strong&gt;minted&lt;br&gt;
on-chain&lt;/strong&gt; (an ERC-1155 token on Polygon). Two completely different write paths —&lt;br&gt;
one is a local DB op, the other is a blockchain transaction confirmed over&lt;br&gt;
minutes.&lt;/p&gt;

&lt;p&gt;And yet the world reads &lt;strong&gt;both the same way&lt;/strong&gt;: a &lt;code&gt;db.map&lt;/code&gt; subscription. A sync&lt;br&gt;
layer mirrors the chain into GenosDB, so the 3D client never waits on an RPC, never&lt;br&gt;
hits the chain on the render path, and works offline. The blockchain is the source&lt;br&gt;
of truth; &lt;strong&gt;GenosDB is the abstraction that makes it feel instant.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What "no server" actually means
&lt;/h2&gt;

&lt;p&gt;The honest question is: if there's no backend, how do peers find each other?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transport:&lt;/strong&gt; WebRTC data channels, peer-to-peer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signaling:&lt;/strong&gt; &lt;a href="https://nostr.com" rel="noopener noreferrer"&gt;Nostr&lt;/a&gt; relays — a decentralized signaling
layer, so there's no central matchmaking server either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict resolution:&lt;/strong&gt; a Hybrid Logical Clock orders concurrent writes
deterministically, so peers converge without a coordinator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result: open two browser tabs, walk around in one, and your avatar appears and&lt;br&gt;
moves in the other — with nothing running in between but the open internet.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's open source — and it documents itself
&lt;/h2&gt;

&lt;p&gt;OVGrid is fully public:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://github.com/estebanrfp/ovgrid" rel="noopener noreferrer"&gt;https://github.com/estebanrfp/ovgrid&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A couple of things worth a look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every tunable in the world — terrain noise, cloud layers, ocean, lighting,
weather, vegetation — is documented parameter-by-parameter in
&lt;a href="https://github.com/estebanrfp/ovgrid/tree/main/docs/configuration" rel="noopener noreferrer"&gt;&lt;code&gt;docs/configuration/&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The repo is indexed by &lt;a href="https://deepwiki.com/estebanrfp/ovgrid" rel="noopener noreferrer"&gt;DeepWiki&lt;/a&gt;, so you
can &lt;em&gt;ask the codebase questions&lt;/em&gt; in natural language.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever wanted to see a serverless, P2P, real-time database do something&lt;br&gt;
harder than a to-do list — go fly around a planet, then read how a single DB holds&lt;br&gt;
it all together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Play:&lt;/strong&gt; &lt;a href="https://world.ovgrid.com" rel="noopener noreferrer"&gt;https://world.ovgrid.com&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/ovgrid" rel="noopener noreferrer"&gt;https://github.com/estebanrfp/ovgrid&lt;/a&gt; · &lt;strong&gt;Database:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;https://github.com/estebanrfp/gdb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webgpu</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Serverless P2P Multiplayer in Godot — No Backend, Just GenosDB</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:21:14 +0000</pubDate>
      <link>https://dev.to/estebanrfp/serverless-p2p-multiplayer-in-godot-no-backend-just-genosdb-1cho</link>
      <guid>https://dev.to/estebanrfp/serverless-p2p-multiplayer-in-godot-no-backend-just-genosdb-1cho</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Try it first:&lt;/strong&gt; open the live demo in &lt;strong&gt;two browser tabs&lt;/strong&gt; → &lt;a href="https://estebanrfp.github.io/godot-genosdb/" rel="noopener noreferrer"&gt;https://estebanrfp.github.io/godot-genosdb/&lt;/a&gt; Each tab is a different player. Walk around, chop a tree — it falls in &lt;em&gt;every&lt;/em&gt; window. Say hi in the chat. No server is running. Anywhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsvn20o1fxi0cakv4xsy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpsvn20o1fxi0cakv4xsy.gif" alt="GenosDB Farm — two browser windows, one shared world, no backend" width="599" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge
&lt;/h2&gt;

&lt;p&gt;Multiplayer usually means servers: a backend, sockets, authoritative state, deployment, scaling, cost. For a small game — or just a prototype — that's a lot of plumbing before the &lt;em&gt;fun&lt;/em&gt; part.&lt;/p&gt;

&lt;p&gt;What if the players' browsers just talked &lt;strong&gt;directly to each other&lt;/strong&gt;, and the shared world simply… persisted, on its own?&lt;/p&gt;

&lt;p&gt;That's what this demo does: a top-down, Stardew-style co-op farm built in &lt;strong&gt;Godot 4&lt;/strong&gt;, exported to the Web, where every connected browser is another farmer in the same world — running on &lt;strong&gt;no backend at all&lt;/strong&gt;. The secret is &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;&lt;strong&gt;GenosDB&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is GenosDB?
&lt;/h2&gt;

&lt;p&gt;GenosDB is a &lt;strong&gt;decentralized graph database that runs entirely in the browser&lt;/strong&gt;. Two parts matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GenosRTC&lt;/strong&gt; — peer-to-peer transport over &lt;strong&gt;WebRTC&lt;/strong&gt;, with &lt;strong&gt;decentralized signaling via Nostr relays&lt;/strong&gt; (no signaling server &lt;em&gt;you&lt;/em&gt; run). Peers find each other and open direct data channels.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A reactive, persistent graph&lt;/strong&gt; — you &lt;code&gt;put&lt;/code&gt; nodes and &lt;code&gt;map&lt;/code&gt; queries, and changes &lt;strong&gt;sync to every peer and persist&lt;/strong&gt; (OPFS). New peers automatically receive the current state.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No backend. No database server. No signaling server. It's all client-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key idea: a hybrid model
&lt;/h2&gt;

&lt;p&gt;Not all multiplayer state is equal, and GenosDB lets you treat it accordingly. The demo splits the world into two layers — and this is the pattern worth stealing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ephemeral&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Player positions &lt;strong&gt;&amp;amp; chat messages&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;a data channel (&lt;code&gt;channel.send&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;fast, ~16 Hz, throwaway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chopped trees + the shared wood total&lt;/td&gt;
&lt;td&gt;the graph (&lt;code&gt;db.put&lt;/code&gt; / &lt;code&gt;db.map&lt;/code&gt; / &lt;code&gt;db.remove&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;synced &lt;strong&gt;and&lt;/strong&gt; stored; late-joiners see the world as it &lt;em&gt;is&lt;/em&gt;, and &lt;strong&gt;Reset&lt;/strong&gt; wipes it for everyone&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That second row is the magic. When you chop a tree you don't &lt;em&gt;shout&lt;/em&gt; "I chopped tree 3!" (only people currently listening hear it). You &lt;strong&gt;write it into a shared graph&lt;/strong&gt; — and anyone who opens a new tab later gets the already-chopped world via GenosDB's &lt;code&gt;initial&lt;/code&gt; event. Chops survive reloads. That's a database, working P2P, for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's a real little game now — and every feature is a GenosDB call
&lt;/h2&gt;

&lt;p&gt;The demo grew from a tech sketch into a cozy co-op game, and each mechanic still maps cleanly to GenosDB:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🪓 &lt;strong&gt;Chop trees with an axe&lt;/strong&gt; — a 4-directional animated farmer. Felled trees &lt;strong&gt;regrow&lt;/strong&gt;, and the &lt;strong&gt;shared wood counter&lt;/strong&gt; (real felled trees, race-free) reads the same for everyone, derived straight from the synced graph.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💬 &lt;strong&gt;P2P chat&lt;/strong&gt; — type a message and it pops as a &lt;strong&gt;speech bubble&lt;/strong&gt; over your farmer in &lt;em&gt;every&lt;/em&gt; window (the ephemeral data channel again).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;♻️ &lt;strong&gt;Reset&lt;/strong&gt; — one button wipes the shared world for all peers. That's &lt;code&gt;db.remove&lt;/code&gt;, propagated P2P.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🎨 &lt;strong&gt;Cozy pixel-art world&lt;/strong&gt;, sound effects and ambient music — and all art &amp;amp; audio are &lt;strong&gt;CC0&lt;/strong&gt; (credits in the repo).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it serverless. All of it P2P.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting Godot to GenosDB
&lt;/h2&gt;

&lt;p&gt;GenosDB is browser-only, and Godot reaches the browser through its &lt;strong&gt;Web (HTML5) export&lt;/strong&gt; + &lt;code&gt;JavaScriptBridge&lt;/code&gt;. So I built a small bridge and packaged it as a plugin — &lt;a href="https://github.com/estebanrfp/godot-genosdb" rel="noopener noreferrer"&gt;&lt;strong&gt;godot-genosdb&lt;/strong&gt;&lt;/a&gt; — that &lt;strong&gt;auto-injects&lt;/strong&gt; the JS bridge into your exported &lt;code&gt;index.html&lt;/code&gt;. Enable it, export to Web, done. Nothing to bundle, no HTML to edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API mirrors GenosDB
&lt;/h2&gt;

&lt;p&gt;On purpose: learn the plugin and you've learned GenosDB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_ready&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_join&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;spawn_farmer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_on_remote_state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# ephemeral (positions + chat)&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graph_changed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_on_world&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# persistent&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"my-room"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="c1"&gt;# = gdb("my-room", {rtc:true})&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt;                                 &lt;span class="c1"&gt;# = db.map({}, cb)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_physics_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_d&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s2"&gt;"x"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"y"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;# = channel.send(...)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;chop_tree&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"tree"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"hp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;"tree_3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# = db.put(node, id)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;reset_world&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"tree_3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                              &lt;span class="c1"&gt;# = db.remove(id)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;GenosDB (JS)&lt;/th&gt;
&lt;th&gt;Godot (&lt;code&gt;Net&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gdb(name, {rtc:true})&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Net.join(name)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;channel.send(data)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Net.send(data)&lt;/code&gt; → signal &lt;code&gt;message&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db.put(node, id)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Net.put(data, id)&lt;/code&gt; → signal &lt;code&gt;graph_changed&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db.map(query, cb)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Net.map(query)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db.remove(id)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Net.remove(id)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;room.on('peer:join' / 'leave')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;signals &lt;code&gt;peer_join&lt;/code&gt; / &lt;code&gt;peer_leave&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it / use it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;▶ Live demo (two tabs!):&lt;/strong&gt; &lt;a href="https://estebanrfp.github.io/godot-genosdb/" rel="noopener noreferrer"&gt;https://estebanrfp.github.io/godot-genosdb/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugin + full source (MIT, assets CC0):&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/godot-genosdb" rel="noopener noreferrer"&gt;https://github.com/estebanrfp/godot-genosdb&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GenosDB:&lt;/strong&gt; &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;https://github.com/estebanrfp/gdb&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install is three steps: copy &lt;code&gt;addons/godot_genosdb/&lt;/code&gt;, enable it in &lt;em&gt;Project Settings → Plugins&lt;/em&gt;, export to Web. P2P is Web-only; on desktop the API is a safe no-op, so the same code runs as single-player in the editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;For jams, prototypes, collaborative toys, or "presence" features, &lt;strong&gt;serverless P2P removes an entire category of work&lt;/strong&gt;. And because GenosDB gives you a &lt;em&gt;persistent reactive graph&lt;/em&gt; — not just a raw WebRTC pipe — your shared world is consistent, survives reconnects, and welcomes late-joiners with zero extra code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two browser tabs. One shared world. No backend. Just GenosDB.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Serverless P2P NFT Marketplace on Polygon — Powered by GenosDB</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Fri, 17 Jul 2026 11:21:12 +0000</pubDate>
      <link>https://dev.to/estebanrfp/serverless-p2p-nft-marketplace-on-polygon-powered-by-genosdb-2ki9</link>
      <guid>https://dev.to/estebanrfp/serverless-p2p-nft-marketplace-on-polygon-powered-by-genosdb-2ki9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Most dapps hammer an RPC and shove a wallet extension in your face. I took the on-chain marketplace from OVGrid and rebuilt it as a ~1,600-line example where the database is the read layer — and your identity is your wallet.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Open two browser tabs at &lt;a href="https://estebanrfp.github.io/dMarket/" rel="noopener noreferrer"&gt;&lt;strong&gt;dMarket&lt;/strong&gt;&lt;/a&gt;. Browse the marketplace in both — no wallet, no sign-up, no spinner waiting on an RPC. The items are just &lt;em&gt;there&lt;/em&gt;, and a change in one tab shows up in the other. That isn't a backend doing the work. It's a peer-to-peer graph database.&lt;/p&gt;

&lt;p&gt;dMarket is a small, faithful slice of the marketplace that runs inside &lt;a href="https://github.com/estebanrfp/ovgrid" rel="noopener noreferrer"&gt;OVGrid&lt;/a&gt; — the WebGPU planet I &lt;a href="https://genosdb.com/webgpu-planet" rel="noopener noreferrer"&gt;wrote about here&lt;/a&gt;. I pulled the blockchain layer out, trimmed it to ~1,600 readable lines of vanilla JavaScript with no build step, and kept it pointed at the &lt;em&gt;same real contracts&lt;/em&gt; OVGrid uses on Polygon. It exists so you can copy a pattern that actually works instead of reinventing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with most blockchain front-ends
&lt;/h2&gt;

&lt;p&gt;Every wallet, every render, every visitor hits the same RPC for the same data. There's no real-time sync between users unless you stand up an indexer and a websocket server — a backend, the very thing crypto was supposed to get rid of. And before anyone can even &lt;em&gt;look&lt;/em&gt;, you make them install MetaMask.&lt;/p&gt;

&lt;p&gt;I wanted to show a different shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what GenosDB is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; is a peer-to-peer graph database that runs entirely in the browser. No central server: data lives locally (OPFS), replicates between peers over WebRTC, and every operation is signed by your own key. You read it reactively with &lt;code&gt;db.map(query, cb)&lt;/code&gt;. (More in the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;examples&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The key idea: the chain is the truth, GenosDB is the mirror
&lt;/h2&gt;

&lt;p&gt;Polygon stays the source of truth for ownership — money is money. But you don't read &lt;em&gt;from&lt;/em&gt; the chain on every render. You read it once, write the result into GenosDB, and from then on everyone reads the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Read the chain (authoritative) once, mirror it into GenosDB:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveListings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;        &lt;span class="c1"&gt;// 2 Multicall3 calls&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;`listing_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listingId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// Everywhere else, read reactively — never the RPC again:&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;repaint&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;            &lt;span class="c1"&gt;// the ONE reactive read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After a buy or a listing confirms on-chain, the same thing happens: &lt;code&gt;db.put&lt;/code&gt; the new state, and &lt;code&gt;db.map&lt;/code&gt; streams it to every connected peer. The read layer never changes whether an item was just discovered on-chain or just created by someone two seconds ago. The database &lt;em&gt;is&lt;/em&gt; the abstraction — fast, offline-capable, peer-to-peer, with no indexer in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm proudest of: your identity is your wallet
&lt;/h2&gt;

&lt;p&gt;GenosDB's Security Manager already gives every user a cryptographic identity — an address and a private key derived from a recovery phrase. Here's the thing nobody points out: &lt;strong&gt;that's an Ethereum account.&lt;/strong&gt; The same key that signs your peer-to-peer database operations signs your Polygon transactions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A GenosDB login hands you a usable wallet. One key, two worlds.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loginOrRecoverUserWithMnemonic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// { address, privateKey }&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wallet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;ethers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Wallet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// signs on-chain txs&lt;/span&gt;
&lt;span class="c1"&gt;// wallet.address === db.sm.getActiveEthAddress()   ✓  no MetaMask&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No extension, no second wallet, no bridging. You log in once and you can both sync P2P and transact on-chain with the exact same identity. dMarket even asserts the two match before it ever signs. See the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;Security Manager docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GenosDB actually buys you here
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What a marketplace needs&lt;/th&gt;
&lt;th&gt;How dMarket does it with GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Live updates between users&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;db.map()&lt;/code&gt; over WebRTC — no indexer, no websocket backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browse instantly, even offline&lt;/td&gt;
&lt;td&gt;local-first reads from GenosDB (OPFS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A wallet&lt;/td&gt;
&lt;td&gt;the GenosDB identity &lt;em&gt;is&lt;/em&gt; the wallet — no MetaMask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Who owns what"&lt;/td&gt;
&lt;td&gt;mirrored P2P, re-derivable from the chain anytime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Simplified, but real
&lt;/h2&gt;

&lt;p&gt;dMarket is deliberately a &lt;em&gt;reference example&lt;/em&gt;, not the full product. It does mint, list, buy and offer — and stops there. No auctions, bundles, rentals, royalties, analytics or a 3-D world; that breadth lives in OVGrid. But what's here is the real, battle-tested integration: real ABIs, real contracts on Polygon Amoy, the real identity bridge. Read dMarket to learn the pattern; study &lt;a href="https://github.com/estebanrfp/ovgrid" rel="noopener noreferrer"&gt;OVGrid&lt;/a&gt; to see it at full scale.&lt;/p&gt;

&lt;p&gt;Browsing works for anyone with no wallet — that's the GenosDB showcase. To actually transact you connect an identity and grab free test POL from the &lt;a href="https://faucet.polygon.technology/" rel="noopener noreferrer"&gt;Amoy faucet&lt;/a&gt;; the README is honest about the prerequisites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it / read it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;▶ &lt;strong&gt;Play it live&lt;/strong&gt; — &lt;a href="https://estebanrfp.github.io/dMarket/" rel="noopener noreferrer"&gt;estebanrfp.github.io/dMarket&lt;/a&gt; (open two tabs)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💻 &lt;strong&gt;Read the code&lt;/strong&gt; — &lt;a href="https://github.com/estebanrfp/dMarket" rel="noopener noreferrer"&gt;github.com/estebanrfp/dMarket&lt;/a&gt; (~1,600 lines, no build)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🌍 &lt;strong&gt;The full world it came from&lt;/strong&gt; — &lt;a href="https://github.com/estebanrfp/ovgrid" rel="noopener noreferrer"&gt;OVGrid&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🗄️ &lt;strong&gt;The database&lt;/strong&gt; — &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/map-guide.md" rel="noopener noreferrer"&gt;reactive reads&lt;/a&gt; · &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-api-reference.md" rel="noopener noreferrer"&gt;identity &amp;amp; security&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;We keep rebuilding the same backend — an indexer here, a websocket there, a wallet popup in front — to do things a &lt;a href="https://genosdb.com/popular-p2p-distributed-databases" rel="noopener noreferrer"&gt;peer-to-peer database&lt;/a&gt; can just &lt;em&gt;do&lt;/em&gt;. dMarket is a small proof that you can put a real blockchain behind a real-time, offline-capable, serverless front end, and have the whole thing be readable in an afternoon.&lt;/p&gt;

&lt;p&gt;The chain is the truth. The database is the mirror. Your identity is your wallet. No backend.&lt;/p&gt;




</description>
      <category>web3</category>
      <category>blockchain</category>
      <category>javascript</category>
      <category>database</category>
    </item>
    <item>
      <title>What Socket.dev Shows About GunDB's Supply Chain in 2026 — and How GenosDB Answers It</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 18:50:43 +0000</pubDate>
      <link>https://dev.to/estebanrfp/what-socketdev-shows-about-gundbs-supply-chain-in-2026-and-how-genosdb-answers-it-3cnb</link>
      <guid>https://dev.to/estebanrfp/what-socketdev-shows-about-gundbs-supply-chain-in-2026-and-how-genosdb-answers-it-3cnb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A technical, source-cited comparison of the install-time supply-chain surface of &lt;code&gt;gun&lt;/code&gt; (GunDB) as reported by Socket.dev, and how &lt;strong&gt;GenosDB&lt;/strong&gt; — a zero-dependency, zero-trust, real-time peer-to-peer graph database — is designed to keep that surface empty. If you are weighing GunDB for production, or looking for a maintained GunDB alternative, this is the security-and-architecture comparison in full.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Reading time: ~18 min. Written by&lt;/em&gt; &lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;&lt;em&gt;Esteban Fuster Pozzi&lt;/em&gt;&lt;/a&gt;&lt;em&gt;, maintainer of GenosDB.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Full disclosure: I built GenosDB, so I have a stake in this comparison — read it knowing that. Here's the rest of the context, so you can weigh it for yourself.&lt;/p&gt;

&lt;p&gt;I spent years working with GunDB — building on it, building its documentation platform, and getting to know genuinely talented developers along the way. GunDB made real-time, offline-first, peer-to-peer data feel possible years before the rest of the ecosystem caught up, and it shaped how I think about decentralized systems.&lt;/p&gt;

&lt;p&gt;GunDB defined a category: a real-time, offline-first, peer-to-peer graph database that runs in the browser. It is a reference point for decentralized, serverless data on the web, alongside projects like OrbitDB, Scuttlebutt and RxDB. GenosDB works in the same category — a distributed graph database with real-time P2P sync — and this post compares it with GunDB on one specific axis: &lt;strong&gt;the security and supply-chain posture of the data/sync layer&lt;/strong&gt;, as measured by an external tool that scans npm packages statically, &lt;a href="https://socket.dev" rel="noopener noreferrer"&gt;Socket.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Socket now provides a complete, itemized report of GunDB's install-time surface. This is the value of the exercise: what used to be described informally ("Gun's dependency tree is heavy") is now a set of concrete, countable facts. This post presents those facts, explains what each alert measures, traces the architectural pattern behind them, and describes how GenosDB's design produces a different report.&lt;/p&gt;

&lt;p&gt;Everything here is verifiable. Every count comes straight from Socket's public pages, quoted as reported.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Socket.dev measures
&lt;/h2&gt;

&lt;p&gt;Socket statically analyzes the &lt;em&gt;behavior&lt;/em&gt; of every package in a dependency graph — whether it spawns a shell, reads environment variables, runs an install script, ships obfuscated or minified blobs, or contacts external URLs — and it does this &lt;strong&gt;transitively&lt;/strong&gt;, across the full tree that an install would resolve.&lt;/p&gt;

&lt;p&gt;Two properties of the method matter for reading the report:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It is transitive.&lt;/strong&gt; The counts below aggregate GunDB's entire dependency graph — every package Gun pulls in, directly and indirectly. A single &lt;code&gt;npm install gun&lt;/code&gt; resolves this whole tree onto your machine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It is behavioral, not just CVE-based.&lt;/strong&gt; Socket flags capabilities and code patterns (install scripts, &lt;code&gt;eval&lt;/code&gt;, native binaries, obfuscation) because these are the primitives that supply-chain attacks use. The &lt;a href="https://socket.dev" rel="noopener noreferrer"&gt;jscrambler npm compromise Socket documented on July 11, 2026&lt;/a&gt; — a malicious &lt;code&gt;preinstall&lt;/code&gt; hook running hidden native binaries across Linux, macOS and Windows — is a recent example of this exact shape.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With that method in mind, here is the report.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F656dab93-596a-4598-86d3-e3cf0f68e0be.png%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F656dab93-596a-4598-86d3-e3cf0f68e0be.png%2520align%3D" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The report: every Socket alert on &lt;code&gt;gun@0.2020.1241&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Source: &lt;code&gt;socket.dev/npm/package/gun/alerts/0.2020.1241?tab=dependencies&lt;/code&gt;. Counts are "instances / packages affected" across Gun's dependency tree, quoted as Socket reports them.&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Known vulnerabilities (CVEs)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Alert&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;th&gt;Reach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Critical CVE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🔴 Vulnerability&lt;/td&gt;
&lt;td&gt;4 instances in 4 packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;High CVE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟠 Vulnerability&lt;/td&gt;
&lt;td&gt;24 instances in 16 packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Medium CVE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟡 Vulnerability&lt;/td&gt;
&lt;td&gt;16 instances in 14 packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Low CVE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vulnerability&lt;/td&gt;
&lt;td&gt;5 instances in 4 packages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dependency tree carries published, catalogued CVEs at every severity tier, including four critical ones. Each is a documented compromise path with an identifier, present in a package Gun depends on. The path to remediation for a dependency CVE is a version bump upstream; as section C shows, that upstream cadence has stopped, so these fixes are not arriving.&lt;/p&gt;

&lt;p&gt;The mechanism is timing: Gun's last release is from 2020, which froze its dependency ranges. Every CVE disclosed in those dependencies since then resolves into a fresh &lt;code&gt;npm install gun&lt;/code&gt; today.&lt;/p&gt;

&lt;h3&gt;
  
  
  B. Supply-chain behavior
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Obfuscated code&lt;/strong&gt; — &lt;em&gt;2 instances in 2 packages.&lt;/em&gt; Files packaged to conceal their behavior. Socket's classification: a potential malware indicator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uses&lt;/strong&gt; &lt;code&gt;eval&lt;/code&gt; &lt;strong&gt;/ dynamic code execution&lt;/strong&gt; — &lt;em&gt;38 instances in 38 packages.&lt;/em&gt; Runtime &lt;code&gt;eval()&lt;/code&gt; constructs and executes code that is absent from the audited source. It bypasses static analysis and requires relaxed CSP to run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install scripts&lt;/strong&gt; — &lt;em&gt;2 instances in 2 packages.&lt;/em&gt; &lt;code&gt;preinstall&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; hooks execute automatically during &lt;code&gt;npm install&lt;/code&gt;, with access to the shell, environment variables, and CI tokens. Two packages in Gun's tree run code at install time — the jscrambler vector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shell access&lt;/strong&gt; — &lt;em&gt;20 instances in 20 packages.&lt;/em&gt; Twenty packages can spawn a system shell, i.e. execute arbitrary commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network access&lt;/strong&gt; — &lt;em&gt;22 instances in 22 packages.&lt;/em&gt; Twenty-two packages can open network connections. Combined with the environment and filesystem access below, this is the full read-secrets-and-transmit capability set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filesystem access&lt;/strong&gt; — &lt;em&gt;78 instances in 78 packages.&lt;/em&gt; Seventy-eight packages can read the disk, and therefore any sensitive data on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment-variable access&lt;/strong&gt; — &lt;em&gt;75 instances in 75 packages.&lt;/em&gt; Seventy-five packages read &lt;code&gt;process.env&lt;/code&gt;, where credentials and tokens live. Socket's classification associates this with credential harvesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URL strings&lt;/strong&gt; — &lt;em&gt;78 instances in 77 packages.&lt;/em&gt; Hardcoded URLs / external IPs the package may contact at runtime — the potential destinations for data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dynamic requires&lt;/strong&gt; — &lt;em&gt;19 instances in 19 packages.&lt;/em&gt; Modules resolved at runtime rather than statically, which loads code a scanner cannot see in advance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debug / reflection access&lt;/strong&gt; — &lt;em&gt;15 instances in 15 packages.&lt;/em&gt; Debug, reflection and dynamic-execution features that expand runtime self-modification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native code&lt;/strong&gt; — &lt;em&gt;2 instances in 2 packages.&lt;/em&gt; Compiled binaries / shared libraries, which are opaque to source review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-detected potential code anomaly&lt;/strong&gt; — &lt;em&gt;52 instances in 50 packages.&lt;/em&gt; Socket's ML flagged unusual behavior across fifty packages for human review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trivial package&lt;/strong&gt; — &lt;em&gt;23 instances in 23 packages.&lt;/em&gt; Twenty-three dependencies under ~10 lines of code. Each is a full third-party trust relationship, and an independent takeover target, for logic that could be inlined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-detected potential typosquats&lt;/strong&gt; — names in the tree close to far more popular packages (Socket names &lt;code&gt;camelcase&lt;/code&gt; and &lt;code&gt;babel-plugin-transform-react-jsx&lt;/code&gt; as the likely targets). Typosquatting is a primary malware-delivery technique.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nonexistent author / New author&lt;/strong&gt; — packages published by an npm account that no longer exists, and (&lt;em&gt;36 instances in 36 packages&lt;/em&gt;) first-time publishers. Both correlate with account-takeover and namespace-hijack events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Potential vulnerability&lt;/strong&gt; — flagged by initial human review, pending confirmation.&lt;/p&gt;

&lt;h3&gt;
  
  
  C. Maintenance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Unmaintained&lt;/strong&gt; — &lt;em&gt;201 instances in 201 packages.&lt;/em&gt; Two hundred and one packages in the tree have gone more than five years without an update. This is the majority of the tree, and it is the reason the CVEs in section A stay open: the upstream cadence that would ship fixes has stopped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deprecated&lt;/strong&gt; — &lt;em&gt;23 instances in 23 packages.&lt;/em&gt; Twenty-three dependencies the maintainers themselves marked as superseded.&lt;/p&gt;

&lt;h3&gt;
  
  
  D. Quality
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Minified code&lt;/strong&gt; — &lt;em&gt;12 instances in 9 packages.&lt;/em&gt; Source shipped minified, which reduces auditability. &lt;strong&gt;Unpopular package&lt;/strong&gt; — &lt;em&gt;2 instances in 2 packages.&lt;/em&gt; Low-usage dependencies, with fewer reviewers. &lt;strong&gt;Floating dependency&lt;/strong&gt; — a dependency on a floating range that can pull a breaking major without notice.&lt;/p&gt;

&lt;h3&gt;
  
  
  E. License
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Multiple license problems&lt;/strong&gt;, &lt;strong&gt;unidentified license&lt;/strong&gt;, &lt;strong&gt;copyleft license&lt;/strong&gt;, &lt;strong&gt;non-permissive license.&lt;/strong&gt; For commercial software, the tree introduces license ambiguity and copyleft/non-permissive terms that carry legal obligations.&lt;/p&gt;




&lt;h2&gt;
  
  
  The pattern behind the numbers
&lt;/h2&gt;

&lt;p&gt;The counts aggregate GunDB's transitive tree, and read together they describe one coherent architectural situation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A large dependency graph — hundreds of packages, the majority unmaintained for five-plus years — carrying four critical CVEs (plus dozens of high and medium) with no upstream path to a fix, alongside install scripts, native binaries, obfuscated files, and typosquat-adjacent names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This describes the &lt;strong&gt;architecture of trust&lt;/strong&gt; the design asks you to accept: a broad, deep dependency tree, taken on faith, frozen since 2020. Two design choices produce it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A broad runtime dependency tree.&lt;/strong&gt; Every convenience pulled from npm is a permanent, transitively-multiplied trust relationship. Gun's tree grew the way most 2018-era JavaScript libraries' trees grew.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A dormant maintenance cadence.&lt;/strong&gt; With the last release in 2020, the tree accumulates CVEs over time and sheds none, because remediation depends on upstream updates that have stopped.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;npm audit fix&lt;/code&gt; operates on this tree; it cannot restructure it. The path to a different report is a different architecture — one that keeps the tree empty from the start. That architecture is the design goal of GenosDB.&lt;/p&gt;




&lt;h2&gt;
  
  
  The maintainer's own account
&lt;/h2&gt;

&lt;p&gt;The maintenance status is also on the record from the source. GunDB's creator, Mark Nadal, characterizes Gun as "actively used and in production" and scalable to millions of monthly users. In the same statement he describes the current maintenance cadence directly: he reached "about 70%" through fixing known bugs, has stepped back from day-to-day upkeep since around 2020, and frames the reduced-support period as running with "no promised timeline" until the next Gun release, pointing users to community forks for prioritized fixes in the meantime.&lt;/p&gt;

&lt;p&gt;These are the maintainer's own words: a production claim on one side, and a paused upstream maintenance cadence with unresolved known bugs on the other. When you choose a foundation to build on for the long term, the useful question is not which claim to believe — it is what you can run and confirm yourself. That is the axis where GenosDB is designed to be checked.&lt;/p&gt;




&lt;h2&gt;
  
  
  How GenosDB produces a different report
&lt;/h2&gt;

&lt;p&gt;GenosDB (GDB) is a distributed, real-time, peer-to-peer graph database — the same category as GunDB. The comparison here is on the same axis: the security and supply-chain posture of the data/sync layer, measured by the same tool.&lt;/p&gt;

&lt;p&gt;Here is &lt;a href="https://socket.dev/npm/package/genosdb" rel="noopener noreferrer"&gt;GenosDB's Socket.dev report&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A self-contained package; the audit target is the package itself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vulnerability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100 / 100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clean.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Supply-chain security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;Strong, and rising as the package ages.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;100+ versions on a weekly release cadence held since inception.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;A single, declared license.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mapped back to Gun's report, class by class:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Zero dependencies
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F58e2e605-88ca-457b-a00e-6d63e07d46b8.png%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F58e2e605-88ca-457b-a00e-6d63e07d46b8.png%2520align%3D" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;GenosDB ships zero runtime dependencies, which Socket confirms: &lt;code&gt;Dependencies: 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every alert in sections B, C, D and E is an alert &lt;em&gt;about a dependency&lt;/em&gt;. A self-contained package resolves the entire category structurally: the audit target is one package, and it is the exact artifact a scanner analyzes, because what ships on npm is the whole product.&lt;/p&gt;

&lt;p&gt;The primitives GenosDB builds on — Ethereum key/signature handling, MessagePack serialization, Pako compression — are pinned to exact versions, reviewed on each bump, and bundled into the published &lt;code&gt;dist&lt;/code&gt; at build time. They live in &lt;code&gt;devDependencies&lt;/code&gt; and travel inside the artifact rather than resolving fresh from npm on each install. This vendored-and-pinned model is why Socket reports &lt;code&gt;0&lt;/code&gt;: at install time, the package is complete on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Clean vulnerability score
&lt;/h3&gt;

&lt;p&gt;With a self-contained package, section A's dependency CVEs are absent, and GenosDB's own code carries a clean vulnerability score (Socket: 100). The active release cadence (section 4) means a future finding has a channel to a fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. A minimal install-time footprint
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Install runs files only.&lt;/strong&gt; GenosDB's &lt;code&gt;package.json&lt;/code&gt; defines no &lt;code&gt;preinstall&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; hooks, so &lt;code&gt;npm install genosdb&lt;/code&gt; copies the package and stops there.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pure JavaScript, pure ESM&lt;/strong&gt; (&lt;code&gt;"type": "module"&lt;/code&gt;). The artifact is readable source, transparent to review.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The sync and security layers are built from static code&lt;/strong&gt;, keeping runtime dynamic execution out of the architecture.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Active maintenance
&lt;/h3&gt;

&lt;p&gt;GenosDB has more than 100 published versions on a weekly release cadence held since the project began, and a maintenance score of 96. Development runs on a &lt;strong&gt;Spec-Driven Development&lt;/strong&gt; method: Esteban Fuster Pozzi as architect and accountable owner, supported by qualified developers and a cluster of audited AI systems whose output is verified against written specifications before it ships. A self-contained base plus a specification-first cadence is what holds the report above stable as the project grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A single declared license
&lt;/h3&gt;

&lt;p&gt;GenosDB ships under one declared license (Socket: License 80), with a &lt;code&gt;THIRD_PARTY_LICENSES.md&lt;/code&gt; accounting for the bundled primitives. A self-contained package keeps license terms singular and explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Bundle, license, and governance
&lt;/h3&gt;

&lt;p&gt;GenosDB's published bundle is free, public, and licensed for commercial use — free to install, inspect, run in production, and ship commercially. Its source is curated on a &lt;strong&gt;Spec-Driven Development&lt;/strong&gt; method — Esteban Fuster Pozzi as accountable architect, qualified developers, and a cluster of audited AI systems working against written specifications — and that process is what keeps the dependency surface at zero: dependencies are chosen deliberately, primitives are pinned and reviewed on each bump, and every change is validated against a spec before it ships. The properties in sections 1–5 are the direct output of that model.&lt;/p&gt;

&lt;p&gt;Auditability tracks the artifact you run. The published bundle is complete JavaScript on npm, and it is exactly what Socket.dev — and you — analyze. You audit the code that executes on your machine.&lt;/p&gt;

&lt;p&gt;A door stays open for the future. As the project matures, additional developers qualified in Spec-Driven Development may be incorporated — selected by consensus — to secure continuity over the very long term, extending the same specification-first discipline that produces the report above.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. The feature surface is demonstrated, not asserted
&lt;/h3&gt;

&lt;p&gt;A database is worth what you can run. GenosDB ships its entire feature surface as self-contained, runnable example apps — more than fifty of them in the repository, each a single HTML file you open and use on the same engine you would deploy. The set spans the whole surface: CRUD and cursor pagination; real-time chat, kanban, collaborative whiteboard and rich-text editors, live cursors and presence; peer-to-peer audio, video and file streaming; geo-spatial queries; the full security suite — RBAC, WebAuthn, node-level ACLs, governance, client-side encryption; and cellular-mesh scaling monitors. Every capability described in this post has a working demo behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F3eb5a2f5-d599-41f3-884b-ce3891d7b98e.png%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F3eb5a2f5-d599-41f3-884b-ce3891d7b98e.png%2520align%3D" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the falsifiable form of a database claim. Rather than "trust that it works," the standing invitation is "open these and watch them work" — the same way you would evaluate any foundation before you build on it. Verification, not belief, is the design stance, from the install-time report down to each example.&lt;/p&gt;




&lt;h2&gt;
  
  
  The data plane: cryptographic enforcement at runtime
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F4f2d6499-028b-44cf-8068-9bfc613e232a.png%2520align%3D" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fuploads%2Fcovers%2F6924809544867700748ef043%2F4f2d6499-028b-44cf-8068-9bfc613e232a.png%2520align%3D" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sections 1–6 concern the install-time surface. A peer-to-peer database also governs a second axis at runtime: once peers are connected, who is authorized to change what. With no server to enforce rules, GenosDB enforces them cryptographically, and each peer verifies every operation independently. From the &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;distributed trust model&lt;/a&gt; and the source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Signed operations.&lt;/strong&gt; Every write is signed by the author's Ethereum key (&lt;code&gt;signOutgoingOperations&lt;/code&gt;) and verified by each receiving peer (&lt;code&gt;verifyIncomingOperations&lt;/code&gt;) before it applies. Identity is cryptographic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero-trust default.&lt;/strong&gt; A new identity is a write-blocked &lt;code&gt;guest&lt;/code&gt; with a single welcome exception — creating its own user node — where the engine forces the role to &lt;code&gt;guest&lt;/code&gt;, so first-message self-promotion resolves to guest. Higher permissions arrive when a &lt;code&gt;superadmin&lt;/code&gt; signs a promotion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RBAC.&lt;/strong&gt; A &lt;code&gt;guest → user → manager → admin → superadmin&lt;/code&gt; hierarchy, enforced locally by each peer's Security Manager. A peer verifies the signed role that authorizes each operation, so a locally-edited client acts within its actual granted role on the network.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Node-level ACLs, enforced across peers.&lt;/strong&gt; Per-node &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt; grants are checked against the cryptographically-verified author of each incoming operation, on every peer. A node accepts writes from its owner and collaborators.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Governance.&lt;/strong&gt; A superadmin declares promotion/demotion rules as native GenosDB queries; the engine resolves each user's role by last-match-wins and signs every change for peers to verify — optionally 24/7 via an always-on &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Fallback Server&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decentralized signaling.&lt;/strong&gt; The real-time layer (GenosRTC) discovers peers over &lt;strong&gt;Nostr relays&lt;/strong&gt; with its own audited crypto/codec, and can encrypt the signaling channel with a shared password.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client-side encryption.&lt;/strong&gt; &lt;code&gt;db.sm.put()&lt;/code&gt; / &lt;code&gt;db.sm.get()&lt;/code&gt; store data end-to-end encrypted — readable by its author and by the addresses they grant, with cryptographic revocation (per-record key rotation) via &lt;code&gt;db.sm.acls&lt;/code&gt;. The envelope format is also faster than the one it replaced, which derived a key with PBKDF2 (100,000 iterations) per record on every read and write — measured in-browser on 0.28: reads 10.9 → 1.2 ms per record, writes 11.1 → 1.5 ms, a 40-record &lt;code&gt;sm.map&lt;/code&gt; ~440 → ~50 ms.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two axes share one design principle: replace trust with verification. Peers verify every operation cryptographically; the package stays self-contained.&lt;/p&gt;




&lt;h2&gt;
  
  
  Downloads, and the last word
&lt;/h2&gt;

&lt;p&gt;A download is a trial, not an outcome. npm counts package fetches — installs, CI runs, mirrors, reinstalls — not developers who shipped a system and kept it running. Popularity measures how many people arrive at a technology, not how many build something durable on it and stay. For a foundational dependency, that gap is where the risk lives: reputation brings developers in the door, and a paused maintenance cadence with unpatchable issues is what some of them find on the other side.&lt;/p&gt;

&lt;p&gt;No download count settles that. A clean install-time report, a self-contained package, and a feature surface you can run exist to put the decision where it belongs — with the developer who tries it. Build the same small thing on each, and let the result speak. The last word is the developer's.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ask
&lt;/h2&gt;

&lt;p&gt;I want help spreading GenosDB, and I'm asking for it directly.&lt;/p&gt;

&lt;p&gt;The value an independent maintainer benefits from most is reach and scrutiny:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the architecture makes sense to you, say so where developers will see it.&lt;/strong&gt; A star on &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;github.com/estebanrfp/gdb&lt;/a&gt;, a reshare, or a mention in a thread where someone is choosing a real-time P2P database moves an independent project forward.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test it adversarially.&lt;/strong&gt; The public bundle and the live demos are enough: run two browsers, try to forge a signed operation or promote past the zero-trust guard, and report what you find via &lt;code&gt;SECURITY.md&lt;/code&gt;. Black-box scrutiny is the highest-value contribution to a security-adjacent project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Port one feature from GunDB.&lt;/strong&gt; A single slice is enough to feel the zero-dependency, zero-trust version and to tell me where it can improve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If you align with this approach and you're strong technically, introduce yourself.&lt;/strong&gt; Contribution opens progressively to a small, vetted, aligned team chosen by consensus, and I'm keeping a list for when that starts.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GunDB defined this category. The goal here is to give it a foundation that a 2026 supply-chain scanner reports clean — and to invite the people who care about that to help prove it, or test it, in public.&lt;/p&gt;

&lt;p&gt;The surface is measured now. Let's build on the one that stays empty.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is GunDB still maintained in 2026?&lt;/strong&gt; GunDB remains widely installed and is used in production. Active upstream maintenance of the current line, though, is paused by the maintainer's own account since around 2020 — known bugs remain, there is no committed timeline for the next release, and users are pointed to community forks for prioritized fixes. Socket.dev's scan reflects it: the majority of Gun's dependency tree is flagged as unmaintained.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is GunDB safe to use in production?&lt;/strong&gt; It depends on your risk tolerance. GunDB has real production deployments and a decade of decentralized-data work in its own source. On the supply-chain axis, Socket.dev reports its dependency tree carries four critical CVEs (plus dozens of high and medium), install scripts, obfuscated code and native binaries, with most of the tree unmaintained for five-plus years — issues without an upstream path to a fix while maintenance is paused. Weigh that install-time surface against your use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a maintained alternative to GunDB?&lt;/strong&gt; GenosDB is a maintained, actively-released alternative in the same category — a distributed, real-time, peer-to-peer graph database in pure JavaScript, reporting zero dependencies and a clean vulnerability score on Socket.dev. Other decentralized options include OrbitDB (on IPFS/libp2p), Scuttlebutt and RxDB; see this &lt;a href="https://genosdb.com/popular-p2p-distributed-databases" rel="noopener noreferrer"&gt;P2P database comparison&lt;/a&gt; for the wider landscape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does GenosDB compare to GunDB?&lt;/strong&gt; Both are real-time, offline-first, peer-to-peer graph databases. GenosDB ships zero runtime dependencies (versus GunDB's flagged transitive tree), a zero-trust security model with signed operations, RBAC, node-level ACLs and governance, and OPFS + Web Worker persistence that keeps the main thread non-blocking (GunDB's default persistence is commonly localStorage, a synchronous ~5 MB store on the main thread). Its real-time layer, GenosRTC, pairs WebRTC with decentralized Nostr signaling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does GenosDB compare to OrbitDB, Scuttlebutt or RxDB?&lt;/strong&gt; OrbitDB builds on IPFS/libp2p; Scuttlebutt is a log-based social protocol; RxDB is a local-first database with pluggable replication. GenosDB is browser-native and serverless, with integrated WebRTC transport and Nostr signaling, a MongoDB-style query layer with recursive graph traversal, and a built-in zero-trust security model — at zero runtime dependencies. Full landscape: &lt;a href="https://genosdb.com/popular-p2p-distributed-databases" rel="noopener noreferrer"&gt;Best P2P &amp;amp; CRDT databases in 2026&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does GenosDB have dependencies?&lt;/strong&gt; Zero, at install time — Socket.dev reports &lt;code&gt;Dependencies: 0&lt;/code&gt;. The primitives it builds on (Ethereum signatures, MessagePack, Pako) are pinned, reviewed and bundled into the published artifact at build time, rather than resolved from npm as a transitive tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is GenosDB free and open source?&lt;/strong&gt; The GenosDB bundle is free, public and licensed for commercial use — free to install, inspect, run in production and ship commercially. The source is curated under a Spec-Driven Development method, and contribution opens progressively to qualified, consensus-selected developers to secure continuity over the long term.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GenosDB&lt;/strong&gt; — distributed, real-time, peer-to-peer graph database. Zero dependencies, zero-trust, serverless. 🔗 &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;github.com/estebanrfp/gdb&lt;/a&gt; · &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;npm: genosdb&lt;/a&gt; · &lt;a href="https://socket.dev/npm/package/genosdb" rel="noopener noreferrer"&gt;Socket report&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Author
&lt;/h2&gt;

&lt;p&gt;Esteban Fuster Pozzi (&lt;a class="mentioned-user" href="https://dev.to/estebanrfp"&gt;@estebanrfp&lt;/a&gt;) - Full Stack JavaScript Developer&lt;/p&gt;




&lt;h3&gt;
  
  
  Sources &amp;amp; verification
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;GunDB Socket report: &lt;code&gt;socket.dev/npm/package/gun/alerts/0.2020.1241?tab=dependencies&lt;/code&gt; (all alert counts quoted verbatim).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GenosDB Socket report: &lt;code&gt;socket.dev/npm/package/genosdb&lt;/code&gt; (scores; 0 dependencies).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GenosDB security model: &lt;code&gt;SECURITY.md&lt;/code&gt;, &lt;code&gt;docs/zero-trust-security-model.md&lt;/code&gt;, &lt;code&gt;docs/genosdb-distributed-trust-model.md&lt;/code&gt;, &lt;code&gt;docs/genosdb-features.md&lt;/code&gt; in the GenosDB repository.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Architecture claims (signing/verification, ACLs, governance, Nostr signaling) verified against the GenosDB, GenosRTC and GenosSRV source.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My personal take
&lt;/h3&gt;

&lt;p&gt;These figures are a snapshot: CVE counts, unmaintained packages and Socket scores all shift as the community patches, so re-run the scan whenever you read this and let today's numbers speak.&lt;/p&gt;

&lt;p&gt;GunDB stays a solid choice for prototypes, internal tools and many wonderful production apps.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This article is part of the official documentation of GenosDB (GDB).&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by &lt;strong&gt;Esteban Fuster Pozzi (&lt;/strong&gt;&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;&lt;strong&gt;estebanrfp&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;)&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;📄 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md" rel="noopener noreferrer"&gt;Whitepaper&lt;/a&gt; | overview of GenosDB design and architecture&lt;/p&gt;

&lt;p&gt;🛠 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;Roadmap&lt;/a&gt; | planned features and future updates&lt;/p&gt;

&lt;p&gt;💡 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md" rel="noopener noreferrer"&gt;Examples&lt;/a&gt; | code snippets and usage demos&lt;/p&gt;

&lt;p&gt;📖 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/index.md" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt; | full reference guide&lt;/p&gt;

&lt;p&gt;🔍 &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md" rel="noopener noreferrer"&gt;API Reference&lt;/a&gt; | detailed API methods&lt;/p&gt;

&lt;p&gt;📚 &lt;a href="https://github.com/estebanrfp/gdb/wiki" rel="noopener noreferrer"&gt;Wiki&lt;/a&gt; | additional notes and guides&lt;/p&gt;

&lt;p&gt;💬 &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; | community questions and feedback&lt;/p&gt;

&lt;p&gt;🗂 &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;Repository&lt;/a&gt; | Minified production-ready files&lt;/p&gt;

&lt;p&gt;📦 &lt;a href="https://www.npmjs.com/package/genosdb" rel="noopener noreferrer"&gt;Install via npm&lt;/a&gt; | quick setup instructions&lt;/p&gt;

&lt;p&gt;🌐 &lt;a href="https://estebanrfp.com/" rel="noopener noreferrer"&gt;Website&lt;/a&gt; | &lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/estebanrfp/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>database</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From Babylon.js to Native WebGPU: How OVGrid Migrated to a Framework-Free Engine</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Tue, 17 Feb 2026 17:43:37 +0000</pubDate>
      <link>https://dev.to/estebanrfp/from-babylonjs-to-native-webgpu-how-ovgrid-migrated-to-a-framework-free-engine-58md</link>
      <guid>https://dev.to/estebanrfp/from-babylonjs-to-native-webgpu-how-ovgrid-migrated-to-a-framework-free-engine-58md</guid>
      <description>&lt;p&gt;In 2020, OVGrid started as an experiment in building dynamic 3D virtual worlds using &lt;strong&gt;Babylon.js&lt;/strong&gt; — one of the most mature WebGL frameworks available. We explored terraforming, dynamic textures, height maps, and real-time rendering. The results were promising.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgv1hjy98nfyefw2ua1n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgv1hjy98nfyefw2ua1n.png" alt="OVGrid development 2020" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OVGrid's first 3D environment tests with Babylon.js (2020)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs3xtekg0cfej2ae0849k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs3xtekg0cfej2ae0849k.png" alt="Exploring Virtual Worlds" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Early terrain generation and texture mapping experiments&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;%[&lt;a href="https://www.youtube.com/watch?v=_hfYsrHMAeI" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=_hfYsrHMAeI&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Babylon.js gave us a solid foundation: a scene graph, a physics engine, material system, and a large community. For a prototype, it was the right choice. But as OVGrid's ambitions grew — planetary-scale terrain, atmospheric scattering, thousands of instanced objects — the framework started showing its limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Left Babylon.js
&lt;/h2&gt;

&lt;p&gt;Babylon.js is an excellent framework. It even added WebGPU support. So why leave?&lt;/p&gt;

&lt;p&gt;The answer isn't about Babylon.js being bad. It's about what happens when you need &lt;strong&gt;total control&lt;/strong&gt; over every GPU instruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundle Size and Dependencies
&lt;/h3&gt;

&lt;p&gt;A full Babylon.js build adds significant weight to your application. For a project that targets instant browser access with zero downloads, every kilobyte matters. OVGrid needed to be lean — a PWA that loads in seconds, even on mobile.&lt;/p&gt;

&lt;h3&gt;
  
  
  GPU Pipeline Control
&lt;/h3&gt;

&lt;p&gt;When you're writing custom atmospheric scattering shaders, cascaded shadow maps with 4 layers, and GPU-driven terrain instancing, you need direct access to the WebGPU pipeline. Working through a framework's abstraction layer means fighting against it when your needs diverge from its assumptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Abstraction Tax
&lt;/h3&gt;

&lt;p&gt;Every abstraction has a cost. Babylon.js manages its own render loop, its own resource lifecycle, its own state. When you need a &lt;strong&gt;Floating Origin&lt;/strong&gt; system that keeps the camera at (0,0,0) and offsets the entire world in Float64 — or when you need to reduce terrain draw calls to &lt;strong&gt;one single call per frame&lt;/strong&gt; — you need the GPU to do exactly what you tell it. No middleware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Philosophy: Pure JavaScript, Zero Frameworks
&lt;/h3&gt;

&lt;p&gt;OVGrid's philosophy aligns with a broader conviction: the best code is the code you fully understand and control. No framework updates breaking your build. No dependency chains. No black boxes. Just JavaScript, WGSL shaders, and the WebGPU API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Migration
&lt;/h2&gt;

&lt;p&gt;The transition wasn't a weekend refactor. It was a deliberate, incremental process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Custom Render Pipeline.&lt;/strong&gt; We replaced Babylon.js's render loop with a direct WebGPU pipeline. This alone gave us the ability to control exactly which compute and render passes execute per frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Shader Rewrite.&lt;/strong&gt; Every shader was rewritten in WGSL from scratch. Atmospheric scattering, terrain rendering, vegetation instancing — all hand-tuned for OVGrid's specific needs. No generic material system overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3 — Floating Origin.&lt;/strong&gt; The camera stays at (0,0,0) in GPU space. World coordinates use Float64 on the CPU, and only relative offsets are sent to the GPU. This eliminates the floating-point jitter that plagues every large-scale WebGL application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4 — GPU-Driven Instancing.&lt;/strong&gt; Terrain chunks, vegetation, and objects are rendered with a single instanced draw call per category. The GPU decides visibility, LOD, and placement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 5 — Modular Architecture.&lt;/strong&gt; Using Bun as the bundler, we split the UI into lazy-loaded modules. Configuration panels load on first interaction. The main bundle stays minimal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;The numbers speak for themselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;120 FPS&lt;/strong&gt; sustained during planetary traversal — from ground level to orbit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One draw call&lt;/strong&gt; for all terrain chunks per frame&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-second&lt;/strong&gt; initial page load as a PWA&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero dependencies&lt;/strong&gt; — no node_modules, no framework updates, no breaking changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full mobile support&lt;/strong&gt; — the same codebase runs on desktop and mobile browsers with WebGPU&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Frameworks Make Sense
&lt;/h2&gt;

&lt;p&gt;This isn't an anti-framework manifesto. Babylon.js is the right choice for many projects — games, product configurators, architectural visualization. If your project fits within a framework's model, use it.&lt;/p&gt;

&lt;p&gt;But when you're building a &lt;strong&gt;planetary-scale virtual world&lt;/strong&gt; that needs to push every boundary of what the browser can do, the framework becomes the ceiling. Removing it becomes the unlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;OVGrid continues to evolve. The migration to native WebGPU was the foundation. On top of it, we're building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt;&lt;/strong&gt; integration for P2P identity and real-time world state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Polygon blockchain&lt;/strong&gt; for OVG tokens and ERC-1155 metaverse assets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready Player Me&lt;/strong&gt; avatars for cross-platform identity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascaded Shadow Maps&lt;/strong&gt; with 4-layer precision up to 10km&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser is no longer the limitation. It's the platform.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;estebanrfp&lt;/a&gt;), creator of &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; and &lt;a href="https://ovgrid.com" rel="noopener noreferrer"&gt;OVGrid&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webgpu</category>
      <category>javascript</category>
      <category>gamedev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Scaling the Metaverse: Building a Planetary-Scale Engine with WebGPU</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Tue, 17 Feb 2026 16:37:58 +0000</pubDate>
      <link>https://dev.to/estebanrfp/scaling-the-metaverse-building-a-planetary-scale-engine-with-webgpu-58mh</link>
      <guid>https://dev.to/estebanrfp/scaling-the-metaverse-building-a-planetary-scale-engine-with-webgpu-58mh</guid>
      <description>&lt;p&gt;How OVGrid is pushing the boundaries of the mobile and desktop web with deep modularization, blockchain integration, and high-precision rendering.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90e6aaf8q6ofc86io3wa.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F90e6aaf8q6ofc86io3wa.jpeg" alt="OVGrid WebGPU Engine" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the rapidly evolving landscape of web-based graphics, the leap from WebGL to &lt;strong&gt;WebGPU&lt;/strong&gt; isn't just an upgrade — it's a paradigm shift. Today, I'm thrilled to share a deep dive into &lt;strong&gt;OVGrid&lt;/strong&gt;, a project designed to prove that the browser can handle massive, planetary-scale environments with the fidelity and performance of native applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Objective: Zero-Latency Exploration
&lt;/h2&gt;

&lt;p&gt;The goal of OVGrid was simple yet ambitious: to create a seamless planetary traversal experience. Whether you are walking on the grass of a procedurally generated biome or orbiting at 500km altitude, the transition should be fluid, and the performance should remain at a solid 120 FPS.&lt;/p&gt;

&lt;p&gt;To achieve this, we've implemented an architecture that prioritizes the "Single Source of Truth" rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU (JavaScript)&lt;/strong&gt;: Decides what exists. It handles the high-precision world coordinates (Float64), Level of Detail (LOD) logic, and physics state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU (WGSL)&lt;/strong&gt;: Decides how it looks. It handles the heavy lifting of ray-tracing, atmospheric scattering, and instanced geometry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deep Modularization: The Phase 6 Milestone
&lt;/h2&gt;

&lt;p&gt;Our latest engineering milestone, &lt;strong&gt;Phase 6: Granular UI Lazy Loading&lt;/strong&gt;, addresses one of the most significant challenges in web apps: the initial payload.&lt;/p&gt;

&lt;p&gt;By splitting our monolithic UI into independent modules — such as &lt;code&gt;EnvironmentUI&lt;/code&gt; and &lt;code&gt;TerrainUI&lt;/code&gt; — we've achieved a state where the browser only downloads and executes the code you actually use.&lt;/p&gt;

&lt;p&gt;Using &lt;strong&gt;Bun&lt;/strong&gt; as our bundler, we've reduced the main bundle size significantly. Now, configuration panels are triggered by fast keyboard shortcuts (&lt;code&gt;T&lt;/code&gt; for Terrain, &lt;code&gt;V&lt;/code&gt; for Environment), loading their specific HTML and logic only on first interaction. This "pay-as-you-go" script execution ensures that the experience remains snappy from the first second of the page load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering the Planet
&lt;/h2&gt;

&lt;p&gt;At the core of the visual experience is a high-precision &lt;strong&gt;Floating Origin&lt;/strong&gt; system. By keeping the camera at (0,0,0) in the shader and passing only relative offsets, we eliminate the floating-point "jitter" that typically plagues large-scale environments.&lt;/p&gt;

&lt;p&gt;Other key technical pillars include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cascaded Shadow Maps (CSM)&lt;/strong&gt;: 4-layer cascades that provide razor-sharp shadows for your avatar while covering terrain up to 10km away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nervland-style LOD Rings&lt;/strong&gt;: An adaptive subdivision system that maintains a highly detailed central block while progressively simplifying the periphery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPU-Driven Instancing&lt;/strong&gt;: We've reduced terrain draw calls from hundreds to &lt;strong&gt;one single call per frame&lt;/strong&gt;, maximizing the efficiency of the WebGPU pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Sovereign Economy
&lt;/h2&gt;

&lt;p&gt;OVGrid isn't just a technical showcase; it's a foundation for a decentralized economy. We have successfully integrated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polygon Blockchain&lt;/strong&gt;: Live connectivity to the Amoy testnet for &lt;strong&gt;OVG Tokens&lt;/strong&gt; and ERC-1155 metaverse assets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital Identity&lt;/strong&gt;: Using &lt;strong&gt;&lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt;&lt;/strong&gt;, we implement WebAuthn biometrics and BIP39 mnemonic recovery, giving users true ownership of their data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Avatars&lt;/strong&gt;: Full integration with &lt;strong&gt;Ready Player Me&lt;/strong&gt;, allowing users to hot-swap their cross-game identities in real-time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Future of Web Graphics
&lt;/h2&gt;

&lt;p&gt;The combination of WebGPU's raw power and modern JavaScript tools like Bun and &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; is opening doors that were previously closed to web developers. OVGrid is a testament to what is possible when you combine low-level optimization with a modular, scalable mindset.&lt;/p&gt;

&lt;p&gt;The horizon is wider than ever.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Esteban Fuster Pozzi (&lt;a href="https://github.com/estebanrfp" rel="noopener noreferrer"&gt;estebanrfp&lt;/a&gt;), creator of &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; and &lt;a href="https://ovgrid.com" rel="noopener noreferrer"&gt;OVGrid&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webgpu</category>
      <category>javascript</category>
      <category>metaverse</category>
      <category>gamedev</category>
    </item>
  </channel>
</rss>
