<?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 Private Messenger With No Server Anywhere: What One P2P Graph Database Can Carry</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Tue, 22 Sep 2026 12:34:46 +0000</pubDate>
      <link>https://dev.to/estebanrfp/a-private-messenger-with-no-server-anywhere-what-one-p2p-graph-database-can-carry-3l67</link>
      <guid>https://dev.to/estebanrfp/a-private-messenger-with-no-server-anywhere-what-one-p2p-graph-database-can-carry-3l67</guid>
      <description>&lt;p&gt;Every peer-to-peer database has a chat demo. It is the &lt;em&gt;hello world&lt;/em&gt; of the genre: two browsers, a shared key, messages appearing on both screens, and the promise that this is what a world without servers looks like. Then you try to add identity, groups, a way to throw someone out, messages that expire — and the demo quietly acquires a server.&lt;/p&gt;

&lt;p&gt;I wanted to know what happens if you refuse. &lt;strong&gt;dMessenger&lt;/strong&gt; is a full private messenger — identity with a recovery phrase and passkeys, one-to-one conversations and groups with a roster, replies and reactions, message lifetimes, attachments and voice notes, invites by link or QR, an installable app that opens with no network — built on &lt;strong&gt;one &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; graph and nothing underneath&lt;/strong&gt;. No relays holding messages. No server anywhere in the data path. And a test suite that proves every one of those claims in real browsers, over real WebRTC, before I was allowed to write them down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it: &lt;a href="https://estebanrfp.github.io/dMessenger/" rel="noopener noreferrer"&gt;estebanrfp.github.io/dMessenger&lt;/a&gt;&lt;/strong&gt; — open it in two windows, or on two devices. &lt;strong&gt;Source: &lt;a href="https://github.com/estebanrfp/dMessenger" rel="noopener noreferrer"&gt;github.com/estebanrfp/dMessenger&lt;/a&gt;&lt;/strong&gt;, MIT.&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%2Fu51np2qpuyuwsf3e7h66.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%2Fu51np2qpuyuwsf3e7h66.png" alt="dMessenger — a group conversation with names, a reply, a reaction and a message lifetime" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where do the messages go when there is no server?
&lt;/h2&gt;

&lt;p&gt;Into the graph — the same one every peer holds. A GenosDB database is named, and everyone who opens that name replicates it, peer to peer, with every operation signed by its author and verified by whoever receives it. There is exactly &lt;strong&gt;one&lt;/strong&gt; database instance in dMessenger, for everything. That was the first design decision and the most important one, so it is worth saying why.&lt;/p&gt;

&lt;p&gt;The obvious alternative — a database per conversation — works, and I measured it working: three instances in one page, three graphs, no bleed. But a second instance in the same page re-initialises the Security Manager and drops the active signer, which you can watch happen as an address that turns &lt;code&gt;null&lt;/code&gt; mid-session. One graph, and everything separated &lt;em&gt;inside&lt;/em&gt; it, along three axes that never touch each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;axis&lt;/th&gt;
&lt;th&gt;mechanism&lt;/th&gt;
&lt;th&gt;what it separates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;kind&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;value.t&lt;/code&gt; + one &lt;code&gt;db.map&lt;/code&gt; query per view&lt;/td&gt;
&lt;td&gt;conversations, messages, identities, keys, vouches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ownership&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;value.owner&lt;/code&gt;, id &lt;code&gt;${owner}:…&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;who may write, link or delete a node — checked by every peer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;confidentiality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;an encrypted record + &lt;code&gt;acls.grant&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;who may &lt;em&gt;read&lt;/em&gt;, enforced by an envelope rather than by topology&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The queries are the routing. The conversation list is a subscription to &lt;code&gt;{ t: 'conv', members: { $in: [me] } }&lt;/code&gt;; the open thread is a subscription to &lt;code&gt;{ t: 'msg', conv: id }&lt;/code&gt;, sorted by the engine. No store, no router, no array to keep in step with the screen. If you have read about &lt;a href="https://genosdb.com/popular-p2p-distributed-databases" rel="noopener noreferrer"&gt;how peer-to-peer databases differ&lt;/a&gt;, this is the part that only works when the database already syncs and already sorts.&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%2Fp3h8psv2ezglya5w0ssx.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%2Fp3h8psv2ezglya5w0ssx.png" alt="One graph, three axes of separation: kind, ownership, confidentiality" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  End-to-end encryption without a key server
&lt;/h2&gt;

&lt;p&gt;A private conversation is two nodes with different jobs. A public one, reactive, says who takes part and where the key lives — metadata every peer sees anyway. An &lt;strong&gt;encrypted record&lt;/strong&gt; (&lt;code&gt;db.sm.put&lt;/code&gt;) holds the key itself, and it reaches each member through &lt;code&gt;acls.grant&lt;/code&gt;: one envelope per reader, wrapped for that reader's published key. Read control is cryptographic. The room replicates every byte of it to everyone, and only the members can open it — which is a test, not a claim: the suite adds a third peer, proves it is replicating the ciphertext, and then proves it cannot read it.&lt;/p&gt;

&lt;p&gt;Each message is an ordinary &lt;strong&gt;public&lt;/strong&gt; node whose &lt;code&gt;body&lt;/code&gt; is ciphertext under that key. Public on purpose: it keeps realtime, ordering and pagination working, which an encrypted query cannot offer, while the content stays sealed. A reaction is a node too, &lt;code&gt;${reactor}:react:${messageId}&lt;/code&gt;, which does three jobs at once — reacting again replaces the earlier one, removing it is a &lt;code&gt;remove&lt;/code&gt; only its owner can sign, and nobody can react in your name, because the id carries the owner the gate enforces. Forged one in the suite; it landed in the forger's copy and nowhere else.&lt;/p&gt;

&lt;p&gt;Groups add the part that key servers usually do for you: removal. Take someone off the roster and two incompatible things must happen — they stop reading what comes next, without destroying what came before for everyone who stayed. So the key record holds an &lt;strong&gt;array&lt;/strong&gt; of keys, one per epoch, and every message carries the epoch it was sealed under. &lt;code&gt;revoke&lt;/code&gt; takes the envelope and rotates the record; a fresh key is appended for whoever still holds one.&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%2Fpo34smbuoen4uu2kzvu1.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%2Fpo34smbuoen4uu2kzvu1.png" alt="Removing a member opens a new key epoch: the remaining members read both epochs, the removed one reads only what came before" width="799" height="387"&gt;&lt;/a&gt; Here it is live, in Bob's window, thirty seconds after he was removed:&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%2Fxpjt7tph7rpr9ba36rmk.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%2Fxpjt7tph7rpr9ba36rmk.png" alt="Bob's window after removal: the message he read while a member is still there; the one after it is sealed for a key he no longer holds" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What he read while he was in is still on his screen — no rotation takes that back, and any design that claims otherwise is lying. What came after is ciphertext for a key he cannot reach. Nobody asked his client to cooperate.&lt;/p&gt;

&lt;p&gt;I should be precise about what this is not. Envelope encryption with a rotated key gives &lt;strong&gt;forward-only access control&lt;/strong&gt;, not the per-message forward secrecy or post-compromise security of a double ratchet. It is honest, it is cheap — 19 µs to seal a kilobyte — and it is what the primitives give you without a server; the &lt;a href="https://genosdb.com/p2p-database-security-audit" rel="noopener noreferrer"&gt;security audit&lt;/a&gt; is where the engine's own guarantees are written down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is a Nostr relay a server?
&lt;/h2&gt;

&lt;p&gt;GenosDB introduces peers over &lt;a href="https://genosdb.com/genosdb-nostr-decentralized-data" rel="noopener noreferrer"&gt;Nostr relays&lt;/a&gt; and then moves the data over WebRTC. The question is fair, and the only honest answer is an experiment you can kill. The performance suite starts a relay it controls, lets two peers meet through it, kills it with &lt;code&gt;SIGKILL&lt;/code&gt;, checks the port is closed — and sends twenty more messages. &lt;strong&gt;Twenty of twenty arrive, p50 45 ms.&lt;/strong&gt; A public relay could not have been killed, and with a list of several, killing one would prove nothing. During 150 messages neither peer makes a single HTTP request; the bytes on the busiest ICE candidate pair say where the data went.&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%2Fcliyv4hs6u5jlor7bjlm.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%2Fcliyv4hs6u5jlor7bjlm.png" alt="The relay introduces, WebRTC carries: killed after the introduction, 20 of 20 messages still arrive" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So: a relay is the phone book. The post goes by hand. That is also why the app scales the way &lt;a href="https://genosdb.com/genosdb-cellular-mesh-p2p-scalability" rel="noopener noreferrer"&gt;the cellular mesh&lt;/a&gt; does rather than the way a chat server does — the room is a community, not a public network of strangers, and I say so in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roles nobody can grant themselves
&lt;/h2&gt;

&lt;p&gt;A new identity is a &lt;code&gt;guest&lt;/code&gt;. It can write — the base role writes, links and deletes on purpose, and ownership already scopes deleting to your own nodes, so the residual risk is spam, never takeover. What it cannot do is become anything else. Roles are set by a superadmin's signed decision, or by &lt;strong&gt;governance rules&lt;/strong&gt; the engine evaluates every four seconds, last match wins:&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&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="s1"&gt;guest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;offsetTimestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;assignRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="s1"&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="s1"&gt;manager&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="s1"&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="s1"&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="s1"&gt;manager&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;vouches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gte&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="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="s1"&gt;manager&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;The first rule keys on time the engine itself observed, the one metric a modified client cannot forge. The third keys on vouches, which live on a node its subject may rewrite — self-service by design, which the guide is explicit about — so the tier it grants is deliberately weak: &lt;code&gt;manager&lt;/code&gt; may &lt;code&gt;publish&lt;/code&gt;, never delete. And &lt;code&gt;publish&lt;/code&gt; is what creating a group costs. That is the whole ladder made real: a &lt;code&gt;user&lt;/code&gt; cannot create a group; two signed vouches later, a &lt;code&gt;manager&lt;/code&gt; can.&lt;/p&gt;

&lt;p&gt;The demo ships the engine's canonical demo identities, so you can watch it without trusting me. Open two windows, press 🛡️ &lt;strong&gt;Superadmin&lt;/strong&gt; in one — its window runs the engine — and 👩‍🦰 &lt;strong&gt;Alice&lt;/strong&gt; in the other:&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;when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alice arrives as &lt;code&gt;guest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the two windows see each other, over public relays&lt;/td&gt;
&lt;td&gt;4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;rule 1 makes her &lt;code&gt;user&lt;/code&gt;&lt;/strong&gt;, signed in the Superadmin's window&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Superadmin and Bob vouch; she publishes her count&lt;/td&gt;
&lt;td&gt;18 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;rule 3 makes her &lt;code&gt;manager&lt;/code&gt;&lt;/strong&gt;; &lt;em&gt;New group&lt;/em&gt; lights up; she creates one; Bob sees it&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22–23 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fh9s80cfwut4339f16p0p.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%2Fh9s80cfwut4339f16p0p.png" alt="Alice's window: manager after two verifiable vouches" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alice's own window says &lt;em&gt;waiting for a superadmin window&lt;/em&gt; the whole time. She never ran the engine. Her role changed anyway, because another peer signed the decision and every peer accepted it. If you know &lt;a href="https://genosdb.com/rbac-role-based-access-control" rel="noopener noreferrer"&gt;role-based access control&lt;/a&gt; from the server side, this is the same vocabulary with the enforcement moved to every receiver.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke, and what it taught me
&lt;/h2&gt;

&lt;p&gt;The engine did not break once. My constitution did, twice, and both times the test suite caught it before a user would have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The manager that kept losing his job.&lt;/strong&gt; The first group test promoted Alice to &lt;code&gt;manager&lt;/code&gt; with a superadmin's signed decision. It passed — then failed a minute later with the button saying &lt;em&gt;you are user&lt;/em&gt;. The governance rules govern the &lt;code&gt;manager&lt;/code&gt; tier, so the floor rule overwrote the manual assignment on the next cycle. Documented behaviour: a rule decides over a term. Either keep standing rules to the tiers below the ones you assign by hand, or earn the tier the way the rule describes. The suite now earns it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The guest who couldn't take it back.&lt;/strong&gt; A &lt;code&gt;guest&lt;/code&gt; retracting a reaction had its &lt;code&gt;remove&lt;/code&gt; refused by every other peer — &lt;code&gt;remove&lt;/code&gt; costs &lt;code&gt;delete&lt;/code&gt;, which I had put above the floor. Since ownership already limits deleting to your own nodes, moving &lt;code&gt;delete&lt;/code&gt; down was safe, and the ladder became honest: the base role can speak and take back what it said.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleting is not disappearing.&lt;/strong&gt; Message lifetimes were the hardest feature, for a reason that has nothing to do with UI. In this engine a tombstone lives only in the operation window; a peer that held the message, went away, and comes back after the tombstone rolled out will bring the node back through its full state. So expiry cannot be "delete on expiry". It is a property of the &lt;em&gt;receiver&lt;/em&gt;, enforced at three points that do not trust each other: no honest peer paints an expired message; a &lt;code&gt;db.use&lt;/code&gt; middleware drops one at the door on every sync path — including a laggard's full state, which arrives as one operation carrying every node and gets filtered node by node; and the author removes its own as housekeeping. The suite shrinks the window to three operations, lets a returning device bring the node back, sends a sentinel to prove it is connected, and asserts nothing expired exists. Live, a 20-second message left both screens 20.5 s after it was sent, and both graphs held zero nodes.&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%2Fsl0jcxds0ury88w7nfz1.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%2Fsl0jcxds0ury88w7nfz1.png" alt="Bob's window: a message with a 20-second countdown" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Measured by the second suite, &lt;code&gt;pnpm perf&lt;/code&gt;, against the application — DOM included — in real Chromium over real WebRTC:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;what&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cold start, engine from the CDN, until the identity door is on screen&lt;/td&gt;
&lt;td&gt;640 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm start&lt;/td&gt;
&lt;td&gt;226 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;identity: key pair, mnemonic, volatile session&lt;/td&gt;
&lt;td&gt;97 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;peer discovery, relay introduction + ICE&lt;/td&gt;
&lt;td&gt;~4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;key exchange: conversation opened, envelope granted and delivered&lt;/td&gt;
&lt;td&gt;125 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delivery latency, 50 messages one by one&lt;/td&gt;
&lt;td&gt;p50 95 · p95 117 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;burst of 100, until all are on the other peer&lt;/td&gt;
&lt;td&gt;242 ms total, 2 ms per message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP requests by either peer during 150 messages&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;relay killed after the introduction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;20/20 delivered&lt;/strong&gt;, p50 45 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;catch-up after a partition, 30 messages missed&lt;/td&gt;
&lt;td&gt;420 ms from page load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-GCM seal / open, 1 KB&lt;/td&gt;
&lt;td&gt;19 µs / 19 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Latency is read the honest way: the sender stamps before sealing, the receiver stamps inside the same &lt;code&gt;db.map&lt;/code&gt; callback the UI paints from, on one machine whose clock both share. That one measurement also fixed a bug — puts take 0 ms, so bursts shared a millisecond and were ordered by random id. Stamps are strictly increasing per session now.&lt;/p&gt;

&lt;h2&gt;
  
  
  From seven lines to a messenger
&lt;/h2&gt;

&lt;p&gt;The engine's own &lt;a href="https://genosdb.com/genosdb-realtime-chat" rel="noopener noreferrer"&gt;realtime chat in seven lines&lt;/a&gt; is where this started, and if you have seen &lt;a href="https://genosdb.com/gundb" rel="noopener noreferrer"&gt;GunDB's chat examples&lt;/a&gt;, you have seen the same &lt;em&gt;hello world&lt;/em&gt;. dMessenger is what that demo turns into when identity, ownership, key distribution, governance and lifetimes are the database's job rather than an afterthought bolted on with a server. It is about 3,100 lines of vanilla JavaScript, three runtime dependencies (identicons, QR generation, QR scanning), and the engine from a pinned CDN URL. Nothing is bundled underneath it.&lt;/p&gt;

&lt;p&gt;It is also the third app in a series that keeps asking the same question — &lt;a href="https://genosdb.com/dcode-p2p-collaborative-code-editor-no-server" rel="noopener noreferrer"&gt;a code editor with no server&lt;/a&gt; came before it — and the first where the answer had to include cryptography that other people's messages depend on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not interoperable with anything.&lt;/strong&gt; Identities are GenosDB addresses; no federation, no bridge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a double ratchet.&lt;/strong&gt; Forward-only access control, as above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a public network.&lt;/strong&gt; A room replicates to everyone in it; the scope of a database is a community.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No push notifications.&lt;/strong&gt; With no server, nothing wakes a closed page — the one thing a serverless design cannot offer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachments are capped at 512 KB&lt;/strong&gt;, because the room replicates every byte to every peer. Larger files belong on an ephemeral channel, not in the graph.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, clone 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/dMessenger &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;dMessenger
pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm &lt;span class="nb"&gt;exec &lt;/span&gt;playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
pnpm &lt;span class="nb"&gt;test&lt;/span&gt;    &lt;span class="c"&gt;# 19 specs, real browsers, real WebRTC, a fresh room per test&lt;/span&gt;
pnpm perf    &lt;span class="c"&gt;# the numbers above, on your machine&lt;/span&gt;
pnpm dev     &lt;span class="c"&gt;# http://localhost:5605&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or skip all that: &lt;a href="https://estebanrfp.github.io/dMessenger/" rel="noopener noreferrer"&gt;open the demo&lt;/a&gt; in two windows, press two buttons, and watch a role change that no client decided.&lt;/p&gt;

</description>
      <category>p2p</category>
      <category>webrtc</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>There Is No Server to Audit — GenosDB's Security Audit, and What It Confirmed</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Sun, 20 Sep 2026 01:59:39 +0000</pubDate>
      <link>https://dev.to/estebanrfp/there-is-no-server-to-audit-genosdbs-first-security-audit-and-what-it-found-3pk4</link>
      <guid>https://dev.to/estebanrfp/there-is-no-server-to-audit-genosdbs-first-security-audit-and-what-it-found-3pk4</guid>
      <description>&lt;p&gt;When you audit a normal database application, the first thing you do is ask where the server is. That is where authorization lives: one process, one place that decides who may read and who may write, one boundary to push against. Find a way past it and you have a finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GenosDB has no server.&lt;/strong&gt; Every peer holds the whole graph, every peer may run modified code, and the relays that carry the traffic are assumed hostile. There is no single place where permission is granted, because permission is not granted anywhere — it is &lt;em&gt;verified&lt;/em&gt;, independently, by every device that receives an operation.&lt;/p&gt;

&lt;p&gt;So what do you audit? That question is more interesting than it sounds, and answering it is what &lt;a href="https://github.com/estebanrfp/gdb/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;the latest release&lt;/a&gt; came out of.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Finding Even Means Without a Server
&lt;/h2&gt;

&lt;p&gt;In a client-server system, the threat model is implicit: the server is honest, the client is not. In GenosDB it has to be stated, because nothing is implicit. It is written down in &lt;a href="https://github.com/estebanrfp/gdb/blob/main/SECURITY.md" rel="noopener noreferrer"&gt;SECURITY.md&lt;/a&gt;, and it is blunt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Any peer may run modified code, and the network — relays, superpeers, other peers — is hostile. A peer decides on its own copy only; every other peer verifies what it receives and applies nothing it cannot verify.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that as an auditor and the target becomes clear. You are not looking for a way past a gate. You are looking for one thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a peer make another peer apply, accept or reveal something outside its authority?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the whole question. Not "can I send a bad operation" — of course you can, you control your own client. The question is whether the &lt;em&gt;receiver&lt;/em&gt; applies it. A modified peer that writes nonsense into its own copy has achieved nothing; it has lied to itself. A modified peer that makes an honest peer store a role it never granted, or open a record it was never given the key to, has found something real.&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%2Fgikb3lyllay72tnd8ajh.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%2Fgikb3lyllay72tnd8ajh.png" alt="What a receiver decides: the signature and the author it recovers, the author's role as this peer holds it, and the node's policy on this peer's own copy — three checks, each read from the receiver's copy and each a refusal on its own; only then is the operation applied and the clock moved." width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This also means a whole class of "vulnerabilities" evaporates on contact. A room opened without the Security Manager has no gate, by design — anyone may write, and that is the documented behaviour of a public room, not a defect. Discipline about that distinction is most of what separates a useful audit from a long list of things that were never promised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closed Source, Open Protocol — and Why That Makes It Auditable
&lt;/h2&gt;

&lt;p&gt;The engine ships minified. That usually ends the conversation with an auditor.&lt;/p&gt;

&lt;p&gt;It does not here, because what the engine &lt;em&gt;does&lt;/em&gt; is fully specified. &lt;a href="https://github.com/estebanrfp/gdb/blob/main/CRYPTOGRAPHY.md" rel="noopener noreferrer"&gt;CRYPTOGRAPHY.md&lt;/a&gt; describes identity derivation, what exactly is signed and in what canonical form, how signatures are verified and authorization decided, ordering and replay, the encrypted-record envelopes, passkeys, signaling and transport, and what is written to disk. Its last section is a list of commands anyone can run against the published bundle to confirm the code matches the document.&lt;/p&gt;

&lt;p&gt;And there is a second layer that matters more than the first: the &lt;a href="https://github.com/estebanrfp/gdb/tree/main/tests/vectors" rel="noopener noreferrer"&gt;conformance vectors&lt;/a&gt;. They are 98 checks re-derived from the specification alone — WebCrypto for HKDF and AES-GCM, &lt;code&gt;@noble/curves&lt;/code&gt; for secp256k1, &lt;code&gt;@noble/hashes&lt;/code&gt; for keccak-256, msgpack and zlib. &lt;strong&gt;No GenosDB code runs in the verifier.&lt;/strong&gt; If the spec and the bundle ever disagree, the vectors say so without anyone having to take my word for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm pack genosdb@latest &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;tar &lt;/span&gt;xzf genosdb-&lt;span class="k"&gt;*&lt;/span&gt;.tgz
node tests/vectors/verify.mjs &lt;span class="nt"&gt;--dist&lt;/span&gt; package/dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;meta.json&lt;/code&gt; pins the SHA-256 of the exact &lt;code&gt;dist/&lt;/code&gt; files the vectors were produced beside, so the bundle you hold can be bound to the numbers you are checking. That is the difference between "trust the source" and "check the artifact".&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Run Was Organized
&lt;/h2&gt;

&lt;p&gt;The audit was run as a structured campaign under Cloudflare's &lt;code&gt;security-audit&lt;/code&gt; skill, with a discipline I would keep regardless of the tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coverage first.&lt;/strong&gt; Twenty-two units, each one a triple of &lt;em&gt;surface × trust boundary × attack class&lt;/em&gt;, so the run could say what it had looked at and, just as importantly, what it had not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hunting and verification are different jobs.&lt;/strong&gt; Every candidate was handed to a verifier that had not found it, and no verdict was accepted without a working reproduction — not an argument, a harness that runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every refusal was re-read.&lt;/strong&gt; A candidate that was dismissed went to a second verifier who was not shown the first conclusion. If a rejection was a misreading, this is what catches it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Black box in parallel.&lt;/strong&gt; A separate matrix of 46 cases, built with a different toolchain and no access to the sources, attacked the published bundles from a hostile browser over real WebRTC — a second graph, a second origin, its own storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing I would emphasise to anyone auditing a P2P system: run the negative controls. "The data appeared on the other peer" proves nothing on its own — it may have arrived through shared storage rather than the network, and a refusal may be a disconnection rather than a decision. Every negative case in that matrix carried a legitimate witness operation that had to arrive by the same path, so a silent failure could never be mistaken for a rejection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Held
&lt;/h2&gt;

&lt;p&gt;Forty of the 46 black-box cases held, against an attacker that controlled its own packets and its own identity: unsigned operations on all four sync paths; tampering with content, id, identity, address, timestamp and signature; guest writes, deletes and links; self-escalation to admin and superadmin; role claims planted in a first profile; demotion; replay of old promotions; ACL nodes without a grant and with a forged owner; a clock run forward; a bounded corpus of invalid signatures; a revoked reader trying to read what came after.&lt;/p&gt;

&lt;p&gt;Twelve of the 22 coverage units closed with nothing to report — both authorship gates against a modified peer, catch-up and ordering, persistence and cross-tab copies, envelope cryptography and passkey custody, transport framing and the cellular overlay, signaling and the embedded relay, the release chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Best Result Was a Design That Answered for Itself
&lt;/h2&gt;

&lt;p&gt;My favourite moment of the whole run was a candidate the design answered on its own.&lt;/p&gt;

&lt;p&gt;It looked clean. &lt;code&gt;assignRole(address, role, expiresAt)&lt;/code&gt; grants a role until a date. Let the date pass, and a standing governance rule promotes that identity straight back to a permanent role. Written as a bug report, it reads like a serious one: an expiry that does not stick.&lt;/p&gt;

&lt;p&gt;Except an expiry in GenosDB is not a countdown some engine owns. It is a term &lt;em&gt;inside a signed decision&lt;/em&gt;, and every peer enforces it locally: past the date, that identity is a guest on every device, with nobody connected and no engine running anywhere. And the project's own decision log says what an expiry promises — it is final &lt;strong&gt;until the constitution decides again&lt;/strong&gt;. A governance rule is the constitution deciding: the engine signs with a superadmin's key or it does not sign at all.&lt;/p&gt;

&lt;p&gt;The verifier proved the other side of it empirically instead of arguing. It took the engine's own operation and signed it with the subject's key: refused by the gate. With a key from outside the constitution: refused. Only an address the clients themselves list as a superadmin can produce that effect — so no lower-trust peer can cause it or benefit from it.&lt;/p&gt;

&lt;p&gt;Then it did the thing I did not expect: it &lt;strong&gt;ran the fix the candidate proposed&lt;/strong&gt;. Applied to the full documented rule ladder, the expired node reads &lt;code&gt;guest&lt;/code&gt;, matches the onboarding rule, and gets promoted to a permanent role anyway. Carry the term forward instead, and two cycles after it lapses you are back at a permanent role — which is precisely the renewal loop that an earlier release removed rule-level expiry to eliminate, more than a year of releases ago.&lt;/p&gt;

&lt;p&gt;An audit rediscovered, from scratch and without being told, why a design decision existed. That is what a written decision log buys you, and it is worth more to me than a report with nothing in it.&lt;/p&gt;

&lt;p&gt;What the candidate left behind is one sentence in the governance guide, stating the term in so many words: a rule decides over a term.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Release Strengthened
&lt;/h2&gt;

&lt;p&gt;The release that followed strengthened four guarantees, each pinned by a test that fails without it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The gate judges what a value carries as well as who signed it.&lt;/strong&gt; On a node with an owner, a write by anyone but the owner carries the owner's policy unchanged — owner, collaborators and the envelope table — so a collaborator writes content, never policy, and a &lt;code&gt;revoke&lt;/code&gt; holds against any later write from an older copy. Three lines of code, in the browser gate and its mirror in the Fallback Server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A governance time objective is the engine's own observation&lt;/strong&gt;, measured from when it first saw a node's current stamp — never the age of a stamp a peer signed for itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The priority path belongs to the constitution.&lt;/strong&gt; A subject's write of its own role node never carries it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six demos render a peer's value as text&lt;/strong&gt;, which is the rule the examples themselves state: no server sits between the writer and the reader.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full report — method, what held, each item the release closed and how, and what this run explicitly did not cover — is published in &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/audits/2026-09-19-genosdb-0.36.0.md" rel="noopener noreferrer"&gt;docs/audits&lt;/a&gt;. The conformance suite came out of it larger than it went in: 84 tests in real browsers over real WebRTC, up from 76, and 107 gate verdicts, up from 87.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audits Have a Home Now
&lt;/h2&gt;

&lt;p&gt;Published audits live in &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/audits/index.md" rel="noopener noreferrer"&gt;docs/audits&lt;/a&gt;: one row and one report per run, with date, reviewer, scope and result. The README points there once and does not change again for an audit.&lt;/p&gt;

&lt;p&gt;If you review GenosDB — as a researcher, a security team, or an organization evaluating it — I will list your report there, findings and all. The specification is public, the bundle is hashable, and the vectors run without a line of GenosDB code. Nothing about this arrangement requires you to believe me.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Where This Comes From
&lt;/h2&gt;

&lt;p&gt;I have spent years building browser P2P databases — I wrote the &lt;a href="https://gun.eco/docs" rel="noopener noreferrer"&gt;official GUN documentation platform&lt;/a&gt; and contributed to that codebase before starting GenosDB. The reason this engine has a decision log, an invariant list and a battery that runs in real browsers over real WebRTC is that I have been on the other side of every one of those absences.&lt;/p&gt;

&lt;p&gt;An audit of a serverless system is not a search for the hole in the wall. There is no wall. There are only receivers, each deciding alone, each having to be right without help. Testing that is harder than testing a server, and it is also the only way to find out whether "zero trust" is an architecture or a slogan.&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;
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>security</category>
      <category>p2p</category>
      <category>database</category>
      <category>javascript</category>
    </item>
    <item>
      <title>SQLite WASM vs GenosDB: Real Benchmarks on a 200k-Relation Dataset</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:41:28 +0000</pubDate>
      <link>https://dev.to/estebanrfp/sqlite-wasm-vs-genosdb-real-benchmarks-on-a-200k-relation-dataset-9c</link>
      <guid>https://dev.to/estebanrfp/sqlite-wasm-vs-genosdb-real-benchmarks-on-a-200k-relation-dataset-9c</guid>
      <description>&lt;p&gt;Most people meet &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;GenosDB&lt;/a&gt; through its P2P side: real-time sync, WebRTC rooms, decentralized apps without a backend. This post is about the other side, the one almost nobody asks about — &lt;strong&gt;GenosDB as a plain local-first store&lt;/strong&gt;, no P2P involved. And instead of a synthetic benchmark, the numbers come from a real app, with a real dataset, against a real incumbent: SQLite compiled to WASM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — on a dataset of 9,783 geo-tagged stops, 4,359 routes and 208,229 relations, GenosDB returned &lt;strong&gt;byte-identical results&lt;/strong&gt; on every query while being &lt;strong&gt;5–100× faster&lt;/strong&gt;, cut first-visit data transfer &lt;strong&gt;25×&lt;/strong&gt;, removed over a megabyte of WASM engine and worker code from the bundle, and eliminated the COOP/COEP header requirement entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;The app is a community bus tracker for a large city, built on a public &lt;a href="https://gtfs.org/" rel="noopener noreferrer"&gt;GTFS&lt;/a&gt; feed. Riders inside a bus become live GPS "feeders" over GenosDB's WebRTC data channels; everyone else watches buses move on a Leaflet map. It is exactly the kind of serverless, community-run product GenosDB was designed for, and it already used GenosDB for the networking side.&lt;/p&gt;

&lt;p&gt;But it carried a second stack alongside. The static GTFS index — every stop, every route, and which routes serve which stops — lived in SQLite via &lt;a href="https://github.com/DallasHoff/sqlocal" rel="noopener noreferrer"&gt;sqlocal&lt;/a&gt;, shipped as a prebuilt 12.5 MB binary that every new visitor downloaded and copied into OPFS.&lt;/p&gt;

&lt;p&gt;Which raised an obvious question: if GenosDB is already in the app for networking, &lt;strong&gt;can it also be the local database?&lt;/strong&gt; We built a working branch to find out. The app itself is not mine to name, so what follows is the engineering, not the case study: the dataset is a public GTFS feed and every number below can be reproduced against one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The swap
&lt;/h2&gt;

&lt;p&gt;The refactor touched exactly two files of app logic (&lt;code&gt;db/client.ts&lt;/code&gt;, &lt;code&gt;db/queries.ts&lt;/code&gt;) plus the build script. No UI component changed. The four query functions kept their exact signatures.&lt;/p&gt;

&lt;p&gt;The storage instance is local-only — note what's &lt;em&gt;not&lt;/em&gt; in the options:&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="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="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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;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="s1"&gt;gtfs-index&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// no rtc — storage only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;rtc&lt;/code&gt; means no signaling, no peers, no sync: just a graph store persisted to OPFS through GenosDB's worker, with spatial queries available out of the box. The P2P instance (&lt;code&gt;rtc: true&lt;/code&gt;) lives separately in the connection layer — static reference data has no business syncing between peers.&lt;/p&gt;

&lt;p&gt;Instead of a 12.5 MB SQLite binary, the app now ships a 2.2 MB &lt;code&gt;gtfs.json&lt;/code&gt; (~500 KB gzipped) and seeds the store on first visit:&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;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;stop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sid&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;longitude&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;// indexed for spatial queries&lt;/span&gt;
  &lt;span class="nx"&gt;routeIds&lt;/span&gt;                            &lt;span class="c1"&gt;// the join table, denormalized&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="s2"&gt;`stop-&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the app's hottest query — "which stops are near the user?" — became a geo query:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&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="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;stop&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// top-level fields AND together&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$near&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;radius&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="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// radius in km&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;h2&gt;
  
  
  Choosing the model: measure, don't guess
&lt;/h2&gt;

&lt;p&gt;The GTFS join table (208k stop↔route pairs) could be modeled two ways: as &lt;strong&gt;graph links&lt;/strong&gt; (&lt;code&gt;db.link()&lt;/code&gt;, one edge per pair) or as &lt;strong&gt;denormalized id arrays&lt;/strong&gt; on the nodes. We benchmarked both at full scale before committing:&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;links (208k edges)&lt;/th&gt;
&lt;th&gt;arrays&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Seed time&lt;/td&gt;
&lt;td&gt;~4.2 s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~300 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm open&lt;/td&gt;
&lt;td&gt;142 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;136 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup queries&lt;/td&gt;
&lt;td&gt;0–11 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0–0.2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Links work fine — 52k links/s sustained is nothing to apologize for — but for this access pattern (fetch a known node, follow its relations) arrays win on every axis, so arrays it is. The graph-y model would have been the &lt;em&gt;showcase&lt;/em&gt; choice; the arrays are the &lt;em&gt;engineering&lt;/em&gt; choice. Ship the engineering choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Everything below was measured on the real dataset — 9,783 stops, 4,359 routes, 208,229 relations — on an M-series MacBook running Chromium, with the same inputs against both engines and results sorted and diffed. &lt;strong&gt;All four queries returned byte-identical results.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query latency:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query&lt;/th&gt;
&lt;th&gt;sqlocal (SQLite WASM)&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;th&gt;speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;closest stops (1 km radius)&lt;/td&gt;
&lt;td&gt;29.7 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6.1 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;routes for a stop (143 rows)&lt;/td&gt;
&lt;td&gt;15.0 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~75×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stops for a route (73 rows)&lt;/td&gt;
&lt;td&gt;11.4 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.1 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~100×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;route search&lt;/td&gt;
&lt;td&gt;2.7 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.1 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~27×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fkuq8na7dirxf7j7lt1xx.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%2Fkuq8na7dirxf7j7lt1xx.png" alt="Query latency compared: SQLite WASM against GenosDB on the same dataset" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ingestion and startup:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;phase&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;put 9,783 geo-tagged stops&lt;/td&gt;
&lt;td&gt;171 ms (~57k ops/s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;put 4,359 routes&lt;/td&gt;
&lt;td&gt;86 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total first-visit seed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~300 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm open, return visits (14k nodes from OPFS)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~140 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Footprint:&lt;/strong&gt;&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;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First-visit data download&lt;/td&gt;
&lt;td&gt;12.5 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~500 KB gzipped&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WASM in the bundle&lt;/td&gt;
&lt;td&gt;860 KB engine + 209 KB worker&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;dist/&lt;/code&gt; total&lt;/td&gt;
&lt;td&gt;19 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.3 MB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COOP/COEP headers&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;not needed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2F3grqosz4q60xqi2qfb0t.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%2F3grqosz4q60xqi2qfb0t.png" alt="What the swap removed: download size, WASM, bundle and deployment headers" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last row deserves a sentence. SQLite WASM needs &lt;code&gt;SharedArrayBuffer&lt;/code&gt;, which needs cross-origin isolation headers on every deploy — the app's own docs flagged them as "breaking change if missing." GenosDB persists through OPFS from a worker without any of that. Deleting &lt;code&gt;netlify.toml&lt;/code&gt; from the repo was the single most satisfying part of the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not just microbenchmarks
&lt;/h2&gt;

&lt;p&gt;Query parity is necessary but not sufficient, so we also ran the entire product on the branch: mocked GPS in a dense downtown area, &lt;strong&gt;two separate browser origins&lt;/strong&gt; — one as a live GPS feeder, one as a watcher. Stop discovery, route search, joining the P2P room, feeder broadcast, cross-origin feeder discovery ("1 bus ~75m" in the route list), and the blue bus marker moving on the watcher's map over real WebRTC with Nostr signaling. The watcher origin seeded its store from scratch along the way.&lt;/p&gt;

&lt;p&gt;We also served the &lt;strong&gt;production build&lt;/strong&gt; (not just the dev server) and re-ran the flow — worth calling out because bundling a library that resolves its own modules dynamically is exactly where builds tend to break. Vite inlines GenosDB's geo module into the main chunk, and every geo query works identically in the bundled app: same 29 stops, clean console.&lt;/p&gt;

&lt;p&gt;Data model, methodology and verification steps were documented alongside the branch, and every measurement above came from the same machine, the same dataset and the same browser, with both engines' outputs sorted and diffed.&lt;/p&gt;

&lt;h2&gt;
  
  
  When should you reach for this?
&lt;/h2&gt;

&lt;p&gt;The pattern generalizes to any app with a chunky, mostly-static reference dataset: product catalogs, map data, dictionaries, game content. The recipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ship the dataset as gzipped JSON (a build script, not a database binary)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gdb('my-store')&lt;/code&gt; — add &lt;code&gt;rtc&lt;/code&gt; only if the data should sync&lt;/li&gt;
&lt;li&gt;Seed once behind your loading screen; version it with a meta node&lt;/li&gt;
&lt;li&gt;Query with &lt;code&gt;$near&lt;/code&gt; for spatial data, &lt;code&gt;db.get&lt;/code&gt; for known ids, &lt;code&gt;db.map&lt;/code&gt; for everything else&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One library, one mental model — and if some of your data &lt;em&gt;should&lt;/em&gt; be shared later, the P2P layer is one option away.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The production builds of GenosDB are free for personal and commercial use; the source is proprietary by deliberate governance choice. Install it with &lt;code&gt;npm i genosdb&lt;/code&gt; — &lt;a href="https://github.com/estebanrfp/gdb" rel="noopener noreferrer"&gt;github.com/estebanrfp/gdb&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://genosdb.com/sqlite-wasm" rel="noopener noreferrer"&gt;genosdb.com/sqlite-wasm&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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>sqlite</category>
      <category>webassembly</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PouchDB vs GenosDB: Offline-First Is Not the Same as Serverless</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:28:50 +0000</pubDate>
      <link>https://dev.to/estebanrfp/pouchdb-vs-genosdb-offline-first-is-not-the-same-as-serverless-5fg3</link>
      <guid>https://dev.to/estebanrfp/pouchdb-vs-genosdb-offline-first-is-not-the-same-as-serverless-5fg3</guid>
      <description>&lt;p&gt;&lt;strong&gt;PouchDB&lt;/strong&gt; is the reason a lot of us believed offline-first was practical at all. It landed in 2012 with a simple, powerful idea: run a database in the browser that speaks the CouchDB replication protocol, and let the two reconcile themselves. That protocol is genuinely excellent — fifteen years of adversarial use, revision trees, deterministic conflict handling — and PouchDB implements it faithfully.&lt;/p&gt;

&lt;p&gt;It is also, and this is the whole point of this article, &lt;strong&gt;a client&lt;/strong&gt;. PouchDB syncs &lt;em&gt;with&lt;/em&gt; something. That something is CouchDB or Cloudant, and it is a server you operate and pay for.&lt;/p&gt;

&lt;p&gt;If your architecture already includes that server, PouchDB is a fine answer and nothing here should talk you out of it. If you picked PouchDB because you wanted offline-first &lt;em&gt;without&lt;/em&gt; a backend, the rest of this is worth reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Offline-First Is Not Serverless
&lt;/h2&gt;

&lt;p&gt;The distinction gets blurred constantly, so it is worth stating plainly: an app can work offline and still require infrastructure. PouchDB gives you the first without giving you the second.&lt;/p&gt;

&lt;p&gt;Ask any AI search engine today for peer-to-peer browser databases and watch PouchDB get excluded by name, with the reason spelled out: it requires a central server such as CouchDB for replication, so it is not truly P2P. That is not a criticism of PouchDB — it is a description of what it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PouchDB                          GenosDB

Browser ──► IndexedDB            Browser ──► Security Manager (signs)
   │                                │
   └──► replication protocol        └──► Graph Store (OPFS)
            │                              │
            ▼                              ├──► Peers (WebRTC)
     CouchDB / Cloudant                    │
     (your server, your bill)              └──► Nostr relays (discovery only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two peers running PouchDB cannot sync with each other. They sync with the same server, and the server makes them agree. Remove it and you have two isolated local databases.&lt;/p&gt;

&lt;p&gt;GenosDB has no such component. Peers exchange signed operations directly over WebRTC, discovery runs through the public &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-architecture.md" rel="noopener noreferrer"&gt;Nostr relay network&lt;/a&gt; — infrastructure that already exists and never sees your data — and authority comes from the signature on each operation rather than from a machine everyone trusts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Project Is Today
&lt;/h2&gt;

&lt;p&gt;PouchDB was donated to the Apache Software Foundation and is now &lt;strong&gt;Apache PouchDB (incubating)&lt;/strong&gt;. Their own &lt;code&gt;DISCLAIMER&lt;/code&gt; file states it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Apache PouchDB (incubating) is an effort undergoing incubation at the Apache Software Foundation (…) While incubation status is not necessarily a reflection of the completeness or stability of the code, it does indicate that the project has yet to be fully endorsed by the ASF."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;This is good news, and I want to be clear about that.&lt;/strong&gt; Moving under the ASF is how a project outlives its original maintainers — governance, trademark, succession. It is exactly the institutional durability that a single-maintainer project like GenosDB does not have, and if that matters to your organisation it is a real argument in PouchDB's favour.&lt;/p&gt;

&lt;p&gt;What it also means is a transition period, and the release cadence shows it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm latest:  9.0.0  —  published June 2024
recent commits: "ASFify README.md and link to DISCLAIMER"
                "add ASF dropdown to main nav"
                dependency bumps
open issues: 189
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Over two years on the same release, with recent activity concentrated on the incubation process rather than on features. Reasonable for a project in transition — just worth knowing before you build on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Open Issues Say
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"IndexedDB adapter can break when opened in multiple tabs" — open since October 2020, 33 comments.&lt;/strong&gt; The most-discussed open issue in the repository. Open your app in two tabs and the adapter can break.&lt;/p&gt;

&lt;p&gt;This one is worth dwelling on, because multi-tab is not an edge case — it is Tuesday. Users open a second tab. In GenosDB, cross-tab coordination is part of the design: tabs share state through &lt;code&gt;BroadcastChannel&lt;/code&gt; while the graph lives in OPFS, so a second tab is just another local reader of the same replica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Duplicate attachments for doc update (&amp;amp; putAttachment)" — open since December 2014, 23 comments.&lt;/strong&gt; Twelve years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"PouchDB in Next.js throws error" — open since September 2023, 16 comments.&lt;/strong&gt; Modern bundler and SSR friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"PouchDB Adapter Memory is broken on Jest 27.x" — open since October 2021, 15 comments.&lt;/strong&gt; Testing against the memory adapter breaks on a five-year-old Jest release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"pouchdb-find: &lt;code&gt;$or&lt;/code&gt; with multiple &lt;code&gt;$regex&lt;/code&gt; and nested fields" — open since July 2022, 9 comments.&lt;/strong&gt; The query layer has gaps at the edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Weight, and the Bill
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pouchdb    5,397 KB unpacked    14 direct dependencies
genosdb    2,007 KB unpacked     0 dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the dependency that matters is not in &lt;code&gt;package.json&lt;/code&gt;. It is CouchDB — a JVM-era server you deploy, secure, back up and scale — or Cloudant, where you rent it by the hour. Every user you add is a row in someone's bill.&lt;/p&gt;

&lt;p&gt;GenosDB's operating cost is structurally zero. There is no server in the data path, so there is nothing that grows with your user count. Peer discovery rides public Nostr relays; if you prefer your own, &lt;a href="https://github.com/estebanrfp/GenosSIG" rel="noopener noreferrer"&gt;GenosSIG&lt;/a&gt; runs an ephemeral relay on Cloudflare's free tier. Neither is a database you operate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Replaces the Server
&lt;/h2&gt;

&lt;p&gt;If the server was doing real work, something has to take over. In GenosDB that something is cryptography.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity and permissions live in the client.&lt;/strong&gt; The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-architecture.md" rel="noopener noreferrer"&gt;Security Manager&lt;/a&gt; provides WebAuthn-based identity and signs every operation with the author's key. &lt;a href="https://genosdb.com/genosdb-rbac-access-control" rel="noopener noreferrer"&gt;Role-based access control&lt;/a&gt; — guest, user, manager, admin, superadmin — plus &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;per-node ACLs&lt;/a&gt; and &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;rule-based governance&lt;/a&gt;. A peer that receives an unauthorised operation rejects it because the signature does not check out, not because a server refused it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflicts resolve without revision trees.&lt;/strong&gt; &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; give causal ordering without trusting device clocks, with a deterministic tie order. When two peers write the same value at once, the writer whose operation lost &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-concurrent-writes.md" rel="noopener noreferrer"&gt;re-applies its edit over the winner&lt;/a&gt; as an ordinary signed write, so neither contribution disappears — and no application code has to resolve a conflict document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sync sends deltas.&lt;/strong&gt; A device rejoining a 210-item space pulls &lt;strong&gt;7.9 KB&lt;/strong&gt;; the same reconciliation without deltas costs 83 KB. That is 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;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queries are reactive.&lt;/strong&gt; &lt;code&gt;$eq $ne $gt $gte $lt $lte $in $between $exists $startsWith $endsWith $contains $text $like $regex $not $and $or $edge $near&lt;/code&gt;, with &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/cursor-based-pagination.md" rel="noopener noreferrer"&gt;cursor pagination&lt;/a&gt; and &lt;code&gt;$edge&lt;/code&gt; for recursive graph traversal. Pass a callback and the query re-fires as peers write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same App, Both Ways
&lt;/h2&gt;

&lt;p&gt;A list that persists locally and syncs between users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PouchDB:&lt;/strong&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;PouchDB&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pouchdb&lt;/span&gt;&lt;span class="dl"&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;new&lt;/span&gt; &lt;span class="nc"&gt;PouchDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes&lt;/span&gt;&lt;span class="dl"&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;remote&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;PouchDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://user:pass@couch.example.com/notes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;live&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;retry&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="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="s1"&gt;change&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;render&lt;/span&gt;&lt;span class="p"&gt;())&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="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&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;all&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;allDocs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;include_docs&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="c1"&gt;// plus: a CouchDB instance, its credentials in the client,&lt;/span&gt;
&lt;span class="c1"&gt;// its users database, and its CORS configuration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GenosDB:&lt;/strong&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="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="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;notes&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="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;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;render&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="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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&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;p&gt;Note the credentials line in the PouchDB version. Syncing straight from the browser means the remote URL — and whatever authenticates it — ships to the client, which is why most production deployments end up putting a proxy in front and writing permission logic there. That proxy is a backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by Side
&lt;/h2&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;PouchDB&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Client of a CouchDB server&lt;/td&gt;
&lt;td&gt;Peer-to-peer, no server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync between two browsers&lt;/td&gt;
&lt;td&gt;Only through the server&lt;/td&gt;
&lt;td&gt;Directly, over WebRTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required infrastructure&lt;/td&gt;
&lt;td&gt;CouchDB or Cloudant&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operating cost&lt;/td&gt;
&lt;td&gt;Grows with users&lt;/td&gt;
&lt;td&gt;Structurally zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local storage&lt;/td&gt;
&lt;td&gt;IndexedDB&lt;/td&gt;
&lt;td&gt;OPFS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple tabs&lt;/td&gt;
&lt;td&gt;Open issue since 2020, 33 comments&lt;/td&gt;
&lt;td&gt;BroadcastChannel, by design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict resolution&lt;/td&gt;
&lt;td&gt;Revision trees, app resolves&lt;/td&gt;
&lt;td&gt;HLC + loser re-applies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;CouchDB &lt;code&gt;_users&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;WebAuthn, signed operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;Server-side&lt;/td&gt;
&lt;td&gt;RBAC + per-node ACLs, client-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data model&lt;/td&gt;
&lt;td&gt;Documents&lt;/td&gt;
&lt;td&gt;Graph with traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;5,397 KB, 14 dependencies&lt;/td&gt;
&lt;td&gt;2,007 KB, zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;Apache Software Foundation&lt;/td&gt;
&lt;td&gt;Single maintainer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latest release&lt;/td&gt;
&lt;td&gt;9.0.0, June 2024&lt;/td&gt;
&lt;td&gt;continuous&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What This Looks Like Running
&lt;/h2&gt;

&lt;p&gt;In the browser, with nothing deployed behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborative block editor — two browsers, one document:&lt;/strong&gt;&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%2Fjf769nwbuml6ff44gqy3.gif" 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%2Fjf769nwbuml6ff44gqy3.gif" alt="GenosDB collaborative block editor syncing between two browsers" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Splitwise-style expense splitter with no backend:&lt;/strong&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%2Fv7w916fok2hsjejfca9k.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%2Fv7w916fok2hsjejfca9k.png" alt="Serverless expense splitter built with GenosDB" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Hacker News clone where moderation is a signed constitution instead of an admin panel:&lt;/strong&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%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="A serverless Hacker News clone built with GenosDB" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When PouchDB Is the Right Choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You already run CouchDB.&lt;/strong&gt; If CouchDB is your source of truth, PouchDB is the best client for it and this comparison is irrelevant to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need the server to be authoritative.&lt;/strong&gt; Server-side validation, audit at a single point, compliance requirements that demand a machine you control. GenosDB verifies cryptographically at every peer, but there is no central authority to appeal to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need institutional governance.&lt;/strong&gt; This is the strongest argument PouchDB has, and it is honest to state it: the ASF provides continuity that does not depend on any one person. GenosDB is maintained by one developer. If your risk assessment weighs that heavily, it should point you to PouchDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want an Apache-licensed dependency.&lt;/strong&gt; PouchDB is Apache 2.0. GenosDB's bundle is &lt;a href="https://github.com/estebanrfp/gdb/blob/main/LICENSE" rel="noopener noreferrer"&gt;free for personal and commercial use&lt;/a&gt;, but the source is proprietary by deliberate governance choice — it is not open source, and describing it as such would be inaccurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GenosDB is the better fit when the server is the thing you are trying to remove: when you want offline-first &lt;em&gt;and&lt;/em&gt; serverless, per-node permissions without a backend to enforce them, and an operating cost that does not scale with your user count.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Where This Comes From
&lt;/h2&gt;

&lt;p&gt;I have spent years building browser P2P databases. I wrote the &lt;a href="https://gun.eco/docs" rel="noopener noreferrer"&gt;official GUN documentation platform&lt;/a&gt; and contributed to that codebase before starting GenosDB, and the design decisions here came from trying to build apps where there was genuinely nothing behind them — no CouchDB, no proxy, no bill that grows with signups.&lt;/p&gt;

&lt;p&gt;PouchDB was never trying to solve that. It was solving "how does a browser app keep working on a train", and it solved it so well that fifteen years later the protocol is still the reference. This article is only about the sentence that tends to get dropped: &lt;em&gt;and then it syncs with your server&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If anything here is out of date or wrong, tell me and I will correct it — &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; is open and I answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://genosdb.com/pouchdb" rel="noopener noreferrer"&gt;genosdb.com/pouchdb&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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>pouchdb</category>
      <category>database</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Automerge vs GenosDB: The Price of Keeping Every Version</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:27:09 +0000</pubDate>
      <link>https://dev.to/estebanrfp/automerge-vs-genosdb-the-price-of-keeping-every-version-5emg</link>
      <guid>https://dev.to/estebanrfp/automerge-vs-genosdb-the-price-of-keeping-every-version-5emg</guid>
      <description>&lt;p&gt;&lt;strong&gt;Automerge&lt;/strong&gt; is the most intellectually ambitious CRDT in JavaScript. It comes out of the Ink &amp;amp; Switch research on local-first software, its core is written in Rust and compiled to WebAssembly, and it does something the others do not: it keeps &lt;strong&gt;the entire history of every change&lt;/strong&gt;, so you can rewind a document, inspect who wrote what, and merge branches of data the way you merge branches of code.&lt;/p&gt;

&lt;p&gt;That is a genuine capability, and when you need it there is no substitute. But it is not free, and the invoice arrives in three places: memory, bundle weight, and the maturity of the layer you need to actually sync anything.&lt;/p&gt;

&lt;p&gt;This article is about that invoice. If you want the argument about &lt;em&gt;how&lt;/em&gt; convergence can work without a CRDT at all, that is a separate piece: &lt;a href="https://genosdb.com/genosdb-collaborative-editing" rel="noopener noreferrer"&gt;real-time collaboration without a CRDT library&lt;/a&gt;. And if you are weighing Automerge against Yjs specifically, &lt;a href="https://genosdb.com/yjs" rel="noopener noreferrer"&gt;that comparison is here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Automerge Keeps
&lt;/h2&gt;

&lt;p&gt;Every CRDT stores metadata beyond your data. Automerge stores more than most, on purpose: a full operation history, per-actor, that survives compaction and travels with the document. That is what makes &lt;code&gt;getChanges&lt;/code&gt;, time travel and attribution possible.&lt;/p&gt;

&lt;p&gt;The consequence is that a document is never just its current state. It is its current state &lt;strong&gt;plus every state it has ever been in&lt;/strong&gt;, and both live in memory while you work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Invoice, Item by Item
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Memory
&lt;/h3&gt;

&lt;p&gt;These are all &lt;strong&gt;open&lt;/strong&gt; issues in the Automerge repository:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Filed&lt;/th&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory leak with &lt;code&gt;initSyncState&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Apr 2024&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9 reactions, &lt;strong&gt;zero comments&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High memory spike on load and save, sometimes causing OOM&lt;/td&gt;
&lt;td&gt;Aug 2023&lt;/td&gt;
&lt;td&gt;8 comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unexpected quadratic performance loading a collaborative document&lt;/td&gt;
&lt;td&gt;Apr 2024&lt;/td&gt;
&lt;td&gt;3 comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Degrading performance over time&lt;/td&gt;
&lt;td&gt;Jul 2024&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Save and Load performance&lt;/td&gt;
&lt;td&gt;May 2023&lt;/td&gt;
&lt;td&gt;6 comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance regression on main&lt;/td&gt;
&lt;td&gt;Sep 2025&lt;/td&gt;
&lt;td&gt;7 comments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiny &lt;code&gt;ReceiveSyncMessage&lt;/code&gt; is slow&lt;/td&gt;
&lt;td&gt;May 2025&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven open issues about memory and speed, and the most-reacted of them — a memory leak in the sync-state initialiser — has been sitting since April 2024 &lt;strong&gt;without a single reply&lt;/strong&gt;. There is also an open request from November 2025 titled &lt;em&gt;"Consider publishing new benchmarks"&lt;/em&gt;, which tells you something about how hard this is to measure from outside.&lt;/p&gt;

&lt;h3&gt;
  
  
  The WebAssembly tax
&lt;/h3&gt;

&lt;p&gt;The Rust core has to reach the browser somehow, and that path is where a lot of developer time goes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Filed&lt;/th&gt;
&lt;th&gt;Comments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The WebAssembly module seems to be missing from the package&lt;/td&gt;
&lt;td&gt;Dec 2023&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundling with Parcel&lt;/td&gt;
&lt;td&gt;Feb 2023&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;wasm.__wbindgen_add_to_stack_pointer&lt;/code&gt; is not a function&lt;/td&gt;
&lt;td&gt;Oct 2023&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deno: &lt;code&gt;automerge_wasm_bg.wasm&lt;/code&gt; is not vendored&lt;/td&gt;
&lt;td&gt;Mar 2023&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running Automerge in a Service Worker&lt;/td&gt;
&lt;td&gt;Oct 2023&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;decodeChange()&lt;/code&gt; panics (WASM &lt;code&gt;capacity_overflow&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Apr 2026&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Panic after &lt;code&gt;migrate_actors&lt;/code&gt; unwrap&lt;/td&gt;
&lt;td&gt;Oct 2025&lt;/td&gt;
&lt;td&gt;open&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every bundler needs its own WASM incantation, and when the Rust side fails it fails as a panic — a &lt;code&gt;capacity_overflow&lt;/code&gt; or an &lt;code&gt;unwrap&lt;/code&gt;, surfacing in your browser console with no JavaScript stack to follow.&lt;/p&gt;

&lt;p&gt;The package itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@automerge/automerge          45,455 KB unpacked
genosdb                        2,007 KB unpacked, zero dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The sync layer is still alpha
&lt;/h3&gt;

&lt;p&gt;Automerge merges documents; it does not move them between devices. For that there is &lt;code&gt;automerge-repo&lt;/code&gt;, the official networking and storage layer. Its current state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm latest:  2.6.0-alpha.3
open issues: 97
other dist-tags: next, authors, subduction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The library you need in order to actually build a collaborative app has never shipped a stable release.&lt;/strong&gt; That is not a hidden fact — the version number says it plainly — but it is easy to miss when you are evaluating Automerge itself, which is at a confident 3.4.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GenosDB Does Instead
&lt;/h2&gt;

&lt;p&gt;GenosDB also keeps an operation log — but for a different purpose, and with a different lifetime.&lt;/p&gt;

&lt;p&gt;The oplog exists so a reconnecting device can receive &lt;strong&gt;only what it missed&lt;/strong&gt; rather than a full state transfer: rejoining a 210-item space costs &lt;strong&gt;7.9 KB&lt;/strong&gt;, where the same reconciliation without deltas costs 83 KB. It is a synchronisation mechanism, described in 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;. What it is not is a permanent per-actor archive that has to be resident in memory for the document to be usable.&lt;/p&gt;

&lt;p&gt;The trade is explicit: &lt;strong&gt;you give up time travel, and you stop paying for it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything else is JavaScript, with no WebAssembly anywhere:&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="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-room&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No bundler configuration, no &lt;code&gt;.wasm&lt;/code&gt; asset to vendor, no panics from a Rust layer — and no separate repo package in alpha, because persistence (OPFS), transport (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;) and peer discovery (public &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-architecture.md" rel="noopener noreferrer"&gt;Nostr relays&lt;/a&gt;, no signaling server of your own) are in the same two-megabyte package.&lt;/p&gt;

&lt;p&gt;And convergence still happens. Concurrent writes to the same value are reconciled 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 a deterministic tie order, and the writer whose operation lost the race &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-concurrent-writes.md" rel="noopener noreferrer"&gt;re-applies its edit over the winner&lt;/a&gt; as an ordinary signed write, so neither contribution is dropped.&lt;/p&gt;

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

&lt;p&gt;Automerge, like Yjs, has no opinion about who you are or what you may do — that is outside its scope. GenosDB puts it inside the database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identity&lt;/strong&gt;, via WebAuthn or a mnemonic, with every operation signed by its author.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions&lt;/strong&gt;: &lt;a href="https://genosdb.com/genosdb-rbac-access-control" rel="noopener noreferrer"&gt;RBAC&lt;/a&gt; with guest/user/manager/admin/superadmin, &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;per-node ACLs&lt;/a&gt;, and &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;rule-based governance&lt;/a&gt;. Enforced by signature at every peer, with no trusted server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries&lt;/strong&gt;, reactive: &lt;code&gt;$eq $gt $in $between $text $like $regex $and $or $edge $near&lt;/code&gt;, with &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/cursor-based-pagination.md" rel="noopener noreferrer"&gt;cursor pagination&lt;/a&gt; and recursive graph traversal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption&lt;/strong&gt;, per record, where each record carries its own wrapped key — a read costs 1.2 ms, against roughly 10.9 ms for the conventional PBKDF2-per-record approach.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Same App, Both Ways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automerge&lt;/strong&gt; — document plus repo plus storage plus network adapter:&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;Repo&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="s1"&gt;@automerge/automerge-repo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;            &lt;span class="c1"&gt;// latest: 2.6.0-alpha.3&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IndexedDBStorageAdapter&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="s1"&gt;@automerge/automerge-repo-storage-indexeddb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BrowserWebSocketClientAdapter&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="s1"&gt;@automerge/automerge-repo-network-websocket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repo&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;Repo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IndexedDBStorageAdapter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BrowserWebSocketClientAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss://your-sync-server.example&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&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;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&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="s1"&gt;hello&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;handle&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="s1"&gt;change&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;doc&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;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;// plus: a WASM asset your bundler has to emit correctly&lt;/span&gt;
&lt;span class="c1"&gt;// plus: identity and permissions, which are yours to build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GenosDB:&lt;/strong&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="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="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-room&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="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;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;render&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="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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&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;h2&gt;
  
  
  Side by Side
&lt;/h2&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;Automerge&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core&lt;/td&gt;
&lt;td&gt;Rust compiled to WebAssembly&lt;/td&gt;
&lt;td&gt;JavaScript, no WASM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Package size&lt;/td&gt;
&lt;td&gt;45,455 KB unpacked&lt;/td&gt;
&lt;td&gt;2,007 KB, zero dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;Full, permanent, in memory&lt;/td&gt;
&lt;td&gt;Oplog for delta sync only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time travel&lt;/td&gt;
&lt;td&gt;Yes — a real strength&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sync layer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;automerge-repo&lt;/code&gt;, &lt;strong&gt;alpha&lt;/strong&gt;, 97 open issues&lt;/td&gt;
&lt;td&gt;Built in, stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Separate adapter package&lt;/td&gt;
&lt;td&gt;OPFS, native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Separate adapter + your server&lt;/td&gt;
&lt;td&gt;WebRTC + public Nostr relays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;WebAuthn / mnemonic, signed ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;RBAC + per-node ACLs + governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;Walk the document in JS&lt;/td&gt;
&lt;td&gt;Full operator set, reactive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;Rust panic in the console&lt;/td&gt;
&lt;td&gt;JavaScript exception&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What This Looks Like Running
&lt;/h2&gt;

&lt;p&gt;In the browser, with nothing deployed behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborative block editor — two browsers, one document:&lt;/strong&gt;&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%2Fjf769nwbuml6ff44gqy3.gif" 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%2Fjf769nwbuml6ff44gqy3.gif" alt="GenosDB collaborative block editor syncing between two browsers" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dCode — branches, commits and pull requests, peer-to-peer:&lt;/strong&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%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, a peer-to-peer collaborative code editor built on GenosDB" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Hacker News clone where moderation is a signed constitution instead of an admin panel:&lt;/strong&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%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="A serverless Hacker News clone built with GenosDB" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When Automerge Is the Right Choice
&lt;/h2&gt;

&lt;p&gt;Be honest with yourself about one question: &lt;strong&gt;do you need the history, or do you just need the merge?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You need time travel or attribution.&lt;/strong&gt; Rewinding a document, showing who changed what, branching and merging data like code — Automerge does this and GenosDB does not. If that is the product, the memory cost is the price of the feature and it is worth paying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are doing research on local-first software.&lt;/strong&gt; Automerge is where that work happens, and the Ink &amp;amp; Switch material around it is the best writing on the subject.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich text with full editing history.&lt;/strong&gt; The combination of a mature text CRDT and a permanent history is genuinely rare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want an open-source dependency.&lt;/strong&gt; Automerge is MIT. GenosDB's bundle is &lt;a href="https://github.com/estebanrfp/gdb/blob/main/LICENSE" rel="noopener noreferrer"&gt;free for personal and commercial use&lt;/a&gt;, but the source is proprietary by deliberate governance choice — it is not open source, and describing it as such would be inaccurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GenosDB is the better fit when the history is not the product: when you want the live collaboration, plus identity, permissions, relations and queries, in one dependency-free package that no bundler has to be taught about.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Where This Comes From
&lt;/h2&gt;

&lt;p&gt;I have spent years building browser P2P databases — I wrote the &lt;a href="https://gun.eco/docs" rel="noopener noreferrer"&gt;official GUN documentation platform&lt;/a&gt; and contributed to that codebase before starting GenosDB. The decision not to keep a permanent operation history was one of the earliest and least comfortable ones, because history is genuinely useful. It was made after watching what it costs in memory on a device that is not a laptop.&lt;/p&gt;

&lt;p&gt;Automerge chose the other side of that trade deliberately, and for the problems it targets it chose correctly. This article is only about making the trade visible before you inherit it.&lt;/p&gt;

&lt;p&gt;If anything here is out of date or wrong, tell me and I will correct it — &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; is open and I answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://genosdb.com/automerge" rel="noopener noreferrer"&gt;genosdb.com/automerge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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>automerge</category>
      <category>crdt</category>
      <category>javascript</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Yjs vs GenosDB: A CRDT Library, or a Database That Already Syncs</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:23:38 +0000</pubDate>
      <link>https://dev.to/estebanrfp/yjs-vs-genosdb-a-crdt-library-or-a-database-that-already-syncs-3c82</link>
      <guid>https://dev.to/estebanrfp/yjs-vs-genosdb-a-crdt-library-or-a-database-that-already-syncs-3c82</guid>
      <description>&lt;p&gt;&lt;strong&gt;Yjs&lt;/strong&gt; is the best CRDT implementation in JavaScript, and it is not close. 31.8 million downloads a month, a decade of work behind it, and a conflict-resolution engine so solid that most of the collaborative editors you have used are running it underneath. Nothing in this article disputes that.&lt;/p&gt;

&lt;p&gt;What this article is about is a different question, and it is the one that decides your architecture: &lt;strong&gt;Yjs is a data structure, not an application.&lt;/strong&gt; It merges concurrent edits. It does not store anything, does not talk to anyone, does not know who the user is and cannot tell you whether they were allowed to make that change. Every one of those lives in a separate package.&lt;/p&gt;

&lt;p&gt;That division is a legitimate design — do one thing well — and for years the ecosystem around it filled the gaps. The problem is what has happened to that ecosystem lately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Yjs Actually Gives You
&lt;/h2&gt;

&lt;p&gt;Yjs provides shared types — &lt;code&gt;Y.Map&lt;/code&gt;, &lt;code&gt;Y.Array&lt;/code&gt;, &lt;code&gt;Y.Text&lt;/code&gt;, &lt;code&gt;Y.XmlFragment&lt;/code&gt; — that converge automatically when several people edit them at once. Feed it updates from anywhere, in any order, possibly duplicated, and every replica ends up identical. That is a genuinely hard problem and Yjs solved it well enough to become the default.&lt;/p&gt;

&lt;p&gt;It is also, deliberately, all it does. Persistence, transport, identity and authorization are somebody else's job — specifically, the providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Provider Problem
&lt;/h2&gt;

&lt;p&gt;This is the part that rarely makes it into comparisons. To turn Yjs into a working application you assemble packages, and their maintenance status is not uniform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Unpacked&lt;/th&gt;
&lt;th&gt;Latest npm release&lt;/th&gt;
&lt;th&gt;Repo activity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;yjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The CRDT itself&lt;/td&gt;
&lt;td&gt;2,253 KB&lt;/td&gt;
&lt;td&gt;current&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y-websocket&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sync via your own server&lt;/td&gt;
&lt;td&gt;92 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Aug 2026&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y-webrtc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Peer-to-peer sync&lt;/td&gt;
&lt;td&gt;1,848 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Dec 2023&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;last push Apr 2024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y-indexeddb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local persistence&lt;/td&gt;
&lt;td&gt;1,097 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Nov 2023&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;last push Feb 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;y-protocols&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Encoding&lt;/td&gt;
&lt;td&gt;1,045 KB&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that table again, because the shape of it matters more than any single row: &lt;strong&gt;the only actively released provider is the one that requires you to run a server.&lt;/strong&gt; The two that would give you peer-to-peer sync and local persistence have not shipped a release in nearly three years, and &lt;code&gt;y-webrtc&lt;/code&gt; has 22 open issues waiting.&lt;/p&gt;

&lt;p&gt;Assembled, the browser-and-P2P stack — &lt;code&gt;yjs&lt;/code&gt; + &lt;code&gt;y-indexeddb&lt;/code&gt; + &lt;code&gt;y-webrtc&lt;/code&gt; + &lt;code&gt;y-protocols&lt;/code&gt; — comes to roughly &lt;strong&gt;6.2 MB unpacked across four packages&lt;/strong&gt;, and still leaves identity, permissions and queries unanswered. GenosDB is 2 MB in one package with zero dependencies, with those three included.&lt;/p&gt;

&lt;p&gt;There is nothing dishonest about this — these are volunteer packages in a large ecosystem, and &lt;code&gt;y-webrtc&lt;/code&gt; still works for many people. But if your plan was "Yjs plus y-webrtc, no backend", you are building on two packages whose last release predates a great deal of what has changed in browsers since.&lt;/p&gt;

&lt;p&gt;Even the community forum reflects the strain: an issue opened in June 2026 reports that &lt;code&gt;discuss.yjs.dev&lt;/code&gt; went down because its Let's Encrypt certificate expired.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Open Issues Say
&lt;/h2&gt;

&lt;p&gt;Yjs has &lt;strong&gt;139 open issues&lt;/strong&gt;. Most are small. These are not:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Nest Y types inside JSON" — open since November 2020, 27 reactions.&lt;/strong&gt; The most-requested change in the repository, six years old. Y types cannot be nested inside plain JSON values, so your data model has to be expressed in Yjs's vocabulary rather than in your own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"TypeScript enhancement" — open since January 2023, 23 reactions&lt;/strong&gt; — and &lt;strong&gt;"Improve typescript typing of &lt;code&gt;.get()&lt;/code&gt;" — open since November 2021, 18 comments.&lt;/strong&gt; &lt;code&gt;Y.Map.get()&lt;/code&gt; cannot tell you what it returns. In practice you cast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Misordered updates result in temporarily missing Y.Map keys" — open since October 2023, 11 comments.&lt;/strong&gt; Under certain orderings, keys can briefly vanish from a &lt;code&gt;Y.Map&lt;/code&gt;. Temporary, but a correctness surprise in a library whose whole promise is convergence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Y.Map get does not return prelim content" — open since June 2020, 11 comments.&lt;/strong&gt; Reading back what you just wrote does not always give you what you wrote.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Still Have to Build
&lt;/h2&gt;

&lt;p&gt;Suppose the providers work perfectly. Here is what Yjs still does not answer, by design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who is this user?&lt;/strong&gt; Yjs has no identity. Awareness carries a nickname and a cursor colour; it is not authentication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Were they allowed to do that?&lt;/strong&gt; There is no authorization layer. Any peer holding the document can modify any part of it. If you need &lt;em&gt;"editors may write, viewers may not"&lt;/em&gt;, you build it, and you build it somewhere trusted — which usually means the server you were trying to avoid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where do I query?&lt;/strong&gt; Yjs has no query language. You walk the structure in JavaScript. There is no "give me the open tasks assigned to me, sorted by priority".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do I relate things?&lt;/strong&gt; Y types nest inside each other, but there are no relations or traversal between documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a flaw. It is what "a CRDT library" means. The question is only whether you want to assemble the rest yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Arrives in One Package Instead
&lt;/h2&gt;

&lt;p&gt;GenosDB starts from the other end: it is a database that happens to synchronise, rather than a synchroniser you build a database around.&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="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-room&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line brings persistence (OPFS), peer-to-peer transport (WebRTC through &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-api-reference.md" rel="noopener noreferrer"&gt;GenosRTC&lt;/a&gt;), peer discovery over the public &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-architecture.md" rel="noopener noreferrer"&gt;Nostr relay network&lt;/a&gt; — no signaling server of your own — cryptographic identity via WebAuthn, and a query engine. Zero dependencies, so there are no satellite packages whose release cadence you have to track.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity and permissions are part of the database.&lt;/strong&gt; The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-architecture.md" rel="noopener noreferrer"&gt;Security Manager&lt;/a&gt; signs every operation with the author's key, and every peer verifies it independently. &lt;a href="https://genosdb.com/genosdb-rbac-access-control" rel="noopener noreferrer"&gt;Role-based access control&lt;/a&gt; — guest, user, manager, admin, superadmin — plus &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;per-node ACLs&lt;/a&gt; and &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;rule-based governance&lt;/a&gt;. "Editors may write, viewers may not" is enforced by signature, with no trusted server anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queries go to an engine, not to a &lt;code&gt;for&lt;/code&gt; loop:&lt;/strong&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="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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;assignee&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="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gte&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="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="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;$limit&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;onRow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// live: re-fires as peers write&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$eq $ne $gt $gte $lt $lte $in $between $exists $startsWith $endsWith $contains $text $like $regex $not $and $or $edge $near&lt;/code&gt;, with &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/cursor-based-pagination.md" rel="noopener noreferrer"&gt;cursor pagination&lt;/a&gt; and &lt;code&gt;$edge&lt;/code&gt; for recursive graph traversal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And convergence still happens&lt;/strong&gt; — without a CRDT library. The mechanism deserves its own article, and it has one: &lt;a href="https://genosdb.com/genosdb-collaborative-editing" rel="noopener noreferrer"&gt;GenosDB vs Yjs and Automerge: real-time collaboration without a CRDT library&lt;/a&gt; walks through the ordering model, one node per paragraph, and why the path is shorter. This article is about the packaging; that one is about the algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same App, Both Ways
&lt;/h2&gt;

&lt;p&gt;A collaborative document that persists locally and syncs peer-to-peer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Yjs:&lt;/strong&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="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Y&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WebrtcProvider&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="s1"&gt;y-webrtc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;          &lt;span class="c1"&gt;// last release: Dec 2023&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IndexeddbPersistence&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="s1"&gt;y-indexeddb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="c1"&gt;// last release: Nov 2023&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&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;Y&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Doc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;IndexeddbPersistence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-room&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebrtcProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-room&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;signaling&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="s1"&gt;wss://your-signaling-server.example&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blocks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&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;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;span class="nx"&gt;blocks&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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}])&lt;/span&gt;

&lt;span class="c1"&gt;// identity, permissions and queries: not included&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GenosDB:&lt;/strong&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="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="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-room&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="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;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;render&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="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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// identity, permissions and queries: already here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Side by Side
&lt;/h2&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;Yjs&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;CRDT library&lt;/td&gt;
&lt;td&gt;Database with sync built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistence&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;y-indexeddb&lt;/code&gt; &lt;em&gt;(Nov 2023)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;OPFS, native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2P transport&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;y-webrtc&lt;/code&gt; &lt;em&gt;(Dec 2023)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;GenosRTC, native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server-free discovery&lt;/td&gt;
&lt;td&gt;Signaling server of your own&lt;/td&gt;
&lt;td&gt;Public Nostr relays&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;None (awareness ≠ auth)&lt;/td&gt;
&lt;td&gt;WebAuthn or mnemonic, signed ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;RBAC + per-node ACLs + governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;Walk the structure in JS&lt;/td&gt;
&lt;td&gt;Full operator set, reactive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relations&lt;/td&gt;
&lt;td&gt;Nested types only&lt;/td&gt;
&lt;td&gt;Graph with &lt;code&gt;$edge&lt;/code&gt; traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packages to track&lt;/td&gt;
&lt;td&gt;4+&lt;/td&gt;
&lt;td&gt;1, zero dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict resolution&lt;/td&gt;
&lt;td&gt;CRDT&lt;/td&gt;
&lt;td&gt;Signed ops + Hybrid Logical Clocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What This Looks Like Running
&lt;/h2&gt;

&lt;p&gt;All of it in the browser, nothing deployed behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborative block editor — two browsers, same document:&lt;/strong&gt;&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%2Fjf769nwbuml6ff44gqy3.gif" 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%2Fjf769nwbuml6ff44gqy3.gif" alt="GenosDB collaborative block editor syncing between two browsers" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dCode — branches, commits and pull requests, peer-to-peer:&lt;/strong&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%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, a peer-to-peer collaborative code editor built on GenosDB" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A collaborative whiteboard in a single file:&lt;/strong&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%2Fbg30knr1uyz2b10pkswv.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%2Fbg30knr1uyz2b10pkswv.png" alt="P2P collaborative whiteboard built with GenosDB" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When Yjs Is the Right Choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You are integrating a rich-text editor.&lt;/strong&gt; ProseMirror, TipTap, Lexical, CodeMirror, Monaco, Slate — Yjs has mature, battle-tested bindings for all of them. If character-level collaborative rich text is the product, use Yjs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You already have a backend and want &lt;code&gt;y-websocket&lt;/code&gt;.&lt;/strong&gt; That path is actively maintained and well documented, and the server does identity and permissions for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need the strongest possible merge guarantees on text.&lt;/strong&gt; Yjs's CRDT has a decade of adversarial use behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want an open-source dependency.&lt;/strong&gt; Yjs is MIT. GenosDB's bundle is &lt;a href="https://github.com/estebanrfp/gdb/blob/main/LICENSE" rel="noopener noreferrer"&gt;free for personal and commercial use&lt;/a&gt; but the source is proprietary by deliberate governance choice — it is not open source, and saying otherwise would be inaccurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GenosDB is the better fit when the document is not the whole product: when you also need identity, permissions, relations and queries, and would rather not assemble four packages — two of them without a release since 2023 — to get there.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Where This Comes From
&lt;/h2&gt;

&lt;p&gt;I have spent years building browser P2P databases. I wrote the &lt;a href="https://gun.eco/docs" rel="noopener noreferrer"&gt;official GUN documentation platform&lt;/a&gt; and contributed to that codebase before starting GenosDB, and every decision described here came from hitting these walls in real projects: a merge engine that worked beautifully and left me writing the authentication, the persistence layer and the permission checks by hand.&lt;/p&gt;

&lt;p&gt;Yjs did not fail at any of that — it never promised it. What changed is that assembling the rest is no longer free, now that the packages you would assemble are three years between releases.&lt;/p&gt;

&lt;p&gt;If anything here is out of date or wrong, tell me and I will correct it — &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; is open and I answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://genosdb.com/yjs" rel="noopener noreferrer"&gt;genosdb.com/yjs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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>yjs</category>
      <category>crdt</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>OrbitDB vs GenosDB: Built on IPFS, or Built on Nothing at All</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:03:14 +0000</pubDate>
      <link>https://dev.to/estebanrfp/orbitdb-vs-genosdb-built-on-ipfs-or-built-on-nothing-at-all-1ffk</link>
      <guid>https://dev.to/estebanrfp/orbitdb-vs-genosdb-built-on-ipfs-or-built-on-nothing-at-all-1ffk</guid>
      <description>&lt;p&gt;&lt;strong&gt;OrbitDB&lt;/strong&gt; did something important: it showed that a serverless, peer-to-peer database in the browser was not a thought experiment. It has been around since 2015, it is built on IPFS and libp2p, it uses Merkle-CRDTs for conflict-free merges, and a lot of what the decentralized web takes for granted today was proven there first.&lt;/p&gt;

&lt;p&gt;It is also the clearest example of a design decision that defines everything downstream: &lt;strong&gt;OrbitDB is a database built on top of IPFS&lt;/strong&gt;. Not one that can use IPFS — one that cannot exist without it. Storage, replication, addressing and identity all run through that layer.&lt;/p&gt;

&lt;p&gt;This article looks at what that choice costs in practice, what OrbitDB's own open issues say about the gaps it left, and how a database designed without that dependency handles the same problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is OrbitDB?
&lt;/h2&gt;

&lt;p&gt;From their README:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"OrbitDB is a serverless, distributed, peer-to-peer database. OrbitDB uses IPFS as its data storage and Libp2p Pubsub to automatically sync databases with peers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It offers several database types (events, documents, keyvalue), an append-only log as the underlying model, and pluggable access controllers and encryption modules. If your project already lives inside the IPFS ecosystem — content addressing, CIDs, pinning services — OrbitDB fits naturally into it, and nothing below changes that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost of Standing on IPFS
&lt;/h2&gt;

&lt;p&gt;Here is the official example from OrbitDB's own documentation for opening a 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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createLibp2p&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="s1"&gt;libp2p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHelia&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="s1"&gt;helia&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createOrbitDB&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="s1"&gt;@orbitdb/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LevelBlockstore&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="s1"&gt;blockstore-level&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Libp2pOptions&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="s1"&gt;./config/libp2p.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockstore&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;LevelBlockstore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ipfs&lt;/span&gt;&lt;span class="dl"&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;libp2p&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;createLibp2p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Libp2pOptions&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;ipfs&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;createHelia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;libp2p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blockstore&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;orbitdb&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;createOrbitDB&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ipfs&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="nx"&gt;orbitdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-db&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;p&gt;Five imports, a blockstore, a libp2p configuration file and a Helia node — before a single record is written. That is not boilerplate that a tutorial skipped; it is the architecture. And it has a weight:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Unpacked&lt;/th&gt;
&lt;th&gt;Direct dependencies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@orbitdb/core&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,577 KB&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;helia&lt;/code&gt; &lt;em&gt;(required)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;5,998 KB&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;libp2p&lt;/code&gt; &lt;em&gt;(required)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;2,768 KB&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ 11.3 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;genosdb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,007 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Resolved transitively, &lt;code&gt;@orbitdb/core&lt;/code&gt; pulls &lt;strong&gt;53 packages&lt;/strong&gt; into a project. GenosDB pulls none — not a small tree, zero — so there is no transitive surface to audit and nothing upstream that can go unmaintained on your behalf.&lt;/p&gt;

&lt;p&gt;In GenosDB the equivalent of everything above is one line:&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="s1"&gt;my-db&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storage is OPFS in the browser. Peer discovery runs over the &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-architecture.md" rel="noopener noreferrer"&gt;Nostr relay network&lt;/a&gt;, which already exists and never sees your data. Transport is WebRTC through &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-api-reference.md" rel="noopener noreferrer"&gt;GenosRTC&lt;/a&gt;. There is no node to boot, no blockstore to configure and no pubsub layer to tune.&lt;/p&gt;

&lt;h2&gt;
  
  
  What OrbitDB's Open Issues Tell You
&lt;/h2&gt;

&lt;p&gt;The most honest way to assess a project is to read what its users asked for and never got. These are all &lt;strong&gt;open&lt;/strong&gt; issues in the OrbitDB repository, with the dates they were filed:&lt;/p&gt;

&lt;h3&gt;
  
  
  "Query standard" — open since August 2018
&lt;/h3&gt;

&lt;p&gt;OrbitDB has no query language. You iterate the log and filter in JavaScript. The request for a standard has sat open, with zero replies, for eight years.&lt;/p&gt;

&lt;p&gt;GenosDB ships a query engine built into &lt;code&gt;map()&lt;/code&gt;: &lt;code&gt;$eq $ne $gt $gte $lt $lte $in $between $exists $startsWith $endsWith $contains $text $like $regex $not $and $or $edge $near&lt;/code&gt;, with sorting, &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; and &lt;code&gt;$edge&lt;/code&gt; for recursive graph traversal. Every query can be made &lt;strong&gt;reactive&lt;/strong&gt; by passing a callback — it re-fires as peers write.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Lamport clock can be set far into future making db progress halt" — open since November 2018
&lt;/h3&gt;

&lt;p&gt;A peer with a wrong clock can push the Lamport counter so far ahead that the database stops making progress. Eight years open, seven comments, unresolved.&lt;/p&gt;

&lt;p&gt;This is precisely the failure mode &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; exist to prevent. GenosDB blends physical time with a logical counter, so causality is preserved without trusting any device's wall clock, and a misconfigured peer cannot stall everyone else.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Ordering tie breaker can cause undefined ordering" — open since November 2018
&lt;/h3&gt;

&lt;p&gt;Two entries with the same identity and timestamp can order unpredictably. Filed in 2018, zero comments since.&lt;/p&gt;

&lt;p&gt;GenosDB defines a deterministic tie order, and for lists that users reorder by hand it uses &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/ordered-lists.md" rel="noopener noreferrer"&gt;fractional order keys&lt;/a&gt;, so inserting, moving and pasting in a shared list converges to the same result on every peer.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Database Encryption &amp;amp; Read Protection" — open since October 2021, 15 comments
&lt;/h3&gt;

&lt;p&gt;OrbitDB does support encryption, through a modular interface where you supply the module. The one the project maintains carries this warning &lt;strong&gt;in OrbitDB's own documentation&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;strong&gt;WARNING:&lt;/strong&gt; SimpleEncryption is an unaudited encryption module. Use at your own risk."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In GenosDB, encryption is not a module you source and vet. The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-architecture.md" rel="noopener noreferrer"&gt;Security Manager&lt;/a&gt; is part of the database: WebAuthn-based identity, signing on every operation, and end-to-end encryption where each record carries its own wrapped key. Reading an encrypted record costs &lt;strong&gt;1.2 ms&lt;/strong&gt;; the conventional approach of deriving a PBKDF2 key per record costs around 10.9 ms, which over a 40-record list is the difference between ~50 ms and nearly two seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Typescript implementation" — open since May 2025, 19 comments
&lt;/h3&gt;

&lt;p&gt;The most-discussed open issue in the repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Database record limits for acceptable lookup performance" — open since February 2024
&lt;/h3&gt;

&lt;p&gt;Users asking where the practical ceiling is. Still open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Access Control: Write Lists, Not Roles
&lt;/h2&gt;

&lt;p&gt;OrbitDB's access controllers, in their own words, "define the &lt;strong&gt;write access&lt;/strong&gt; a user has to a database". You get two: an immutable one stored on IPFS and a mutable one backed by an OrbitDB keyvalue store. Both answer a single question — &lt;em&gt;is this identity allowed to write to this database?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That leaves three things to you: read protection, granularity below the database level, and any notion of roles.&lt;/p&gt;

&lt;p&gt;GenosDB answers all three inside the client. &lt;a href="https://genosdb.com/genosdb-rbac-access-control" rel="noopener noreferrer"&gt;Role-based access control&lt;/a&gt; with a real hierarchy — guest, user, manager, admin, superadmin — &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;ACLs at the level of individual nodes&lt;/a&gt;, and &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;rule-based governance&lt;/a&gt; that promotes and demotes users automatically according to signed rules. Confidentiality is cryptographic rather than topological: a peer can relay data it is not allowed to read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability Without a Pinning Service
&lt;/h2&gt;

&lt;p&gt;Because OrbitDB stores data as IPFS blocks, a record survives only while some peer still holds that block. Pinning — a paid service or an always-on node — is therefore a &lt;strong&gt;durability requirement&lt;/strong&gt;, not a convenience: stop pinning and the data can genuinely go away.&lt;/p&gt;

&lt;p&gt;GenosDB inverts the relationship. Every peer keeps a &lt;strong&gt;full local replica in OPFS&lt;/strong&gt;, so the data already exists, complete, on every device that has ever opened the app. Reading it needs no network at all, and nothing is lost when peers disconnect — there is no block to keep alive somewhere else. A device that comes back pulls only what it missed: rejoining a 210-item space costs &lt;strong&gt;7.9 KB&lt;/strong&gt;, where the same reconciliation without deltas costs 83 KB, thanks to 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;.&lt;/p&gt;

&lt;p&gt;The optional &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-fallback-server.md" rel="noopener noreferrer"&gt;always-on peer&lt;/a&gt; solves a different problem — not keeping data alive, but greeting a brand-new device that arrives when nobody else happens to be online. It is a peer you may choose to run, not infrastructure the design leans on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Same App, Both Ways
&lt;/h2&gt;

&lt;p&gt;A shared list that syncs live between browsers. This is OrbitDB's own getting-started example, unedited:&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;createLibp2p&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="s1"&gt;libp2p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHelia&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="s1"&gt;helia&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createOrbitDB&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="s1"&gt;@orbitdb/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LevelBlockstore&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="s1"&gt;blockstore-level&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Libp2pOptions&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="s1"&gt;./config/libp2p.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockstore&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;LevelBlockstore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ipfs/blocks&lt;/span&gt;&lt;span class="dl"&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;libp2p&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;createLibp2p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Libp2pOptions&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;ipfs&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;createHelia&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;libp2p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;blockstore&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;orbitdb&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;createOrbitDB&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;ipfs&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="nx"&gt;orbitdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-db&lt;/span&gt;&lt;span class="dl"&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello world 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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="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;all&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;close&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;orbitdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&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;ipfs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note what &lt;code&gt;all()&lt;/code&gt; is: a point-in-time read. To know when a peer writes something you subscribe to database events separately, and to find records matching a condition you load everything and filter in JavaScript — there is no query layer to push that down to.&lt;/p&gt;

&lt;p&gt;The same thing in GenosDB:&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="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-db&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="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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello world 1&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;// live: the callback re-fires as peers write&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;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;render&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when the list is not "everything", the query goes to the engine rather than to a &lt;code&gt;.filter()&lt;/code&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="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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gte&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="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&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="s1"&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="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="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;$limit&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;onRow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no libp2p configuration file because there is no libp2p, no blockstore because storage is OPFS, and no &lt;code&gt;ipfs.stop()&lt;/code&gt; because there is no node to stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by Side
&lt;/h2&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;OrbitDB&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foundation&lt;/td&gt;
&lt;td&gt;IPFS + libp2p, required&lt;/td&gt;
&lt;td&gt;None — runs standalone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install weight&lt;/td&gt;
&lt;td&gt;≈ 11.3 MB, 53 transitive packages&lt;/td&gt;
&lt;td&gt;2 MB, zero dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;blockstore + libp2p + Helia + OrbitDB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;await gdb(name, { rtc: true })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data model&lt;/td&gt;
&lt;td&gt;Append-only log (events, docs, keyvalue)&lt;/td&gt;
&lt;td&gt;Graph with relations and traversal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;None — iterate and filter in JS&lt;/td&gt;
&lt;td&gt;Full operator set, reactive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict resolution&lt;/td&gt;
&lt;td&gt;Merkle-CRDTs + Lamport clocks&lt;/td&gt;
&lt;td&gt;Hybrid Logical Clocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;Modular; official module is unaudited&lt;/td&gt;
&lt;td&gt;Built in, per-record wrapped keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control&lt;/td&gt;
&lt;td&gt;Write lists per database&lt;/td&gt;
&lt;td&gt;RBAC + node-level ACLs + governance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Keypair&lt;/td&gt;
&lt;td&gt;WebAuthn or mnemonic, signed operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability&lt;/td&gt;
&lt;td&gt;Survives only while a peer pins the block&lt;/td&gt;
&lt;td&gt;Full replica on every device, always&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;td&gt;Open issue since 2025&lt;/td&gt;
&lt;td&gt;Typed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What This Looks Like Running
&lt;/h2&gt;

&lt;p&gt;Everything below runs in the browser with nothing deployed behind it — no IPFS node, no pinning service, no backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborative editing, no CRDT library:&lt;/strong&gt;&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%2Fjf769nwbuml6ff44gqy3.gif" 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%2Fjf769nwbuml6ff44gqy3.gif" alt="GenosDB collaborative block editor syncing between two browsers" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dCode — a code editor with branches, commits and pull requests, peer-to-peer:&lt;/strong&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%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, a peer-to-peer collaborative code editor built on GenosDB" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Hacker News clone where moderation is a signed constitution instead of an admin panel:&lt;/strong&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%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="A serverless Hacker News clone built with GenosDB" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A collaborative whiteboard in a single file:&lt;/strong&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%2Fbg30knr1uyz2b10pkswv.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%2Fbg30knr1uyz2b10pkswv.png" alt="P2P collaborative whiteboard built with GenosDB" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When OrbitDB Is the Right Choice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You are already on IPFS.&lt;/strong&gt; If your data is content-addressed, if CIDs matter to your model, or if you are pinning through an existing service, OrbitDB is native to that world and GenosDB is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need a tamper-evident archive.&lt;/strong&gt; OrbitDB's append-only Merkle log is content-addressed, so history cannot be quietly rewritten — valuable if auditability &lt;em&gt;is&lt;/em&gt; the product. GenosDB signs every operation and keeps an oplog for synchronisation, but it is built to represent current state rather than to serve as a permanent ledger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a fully open-source stack.&lt;/strong&gt; OrbitDB is MIT. GenosDB's production bundle is &lt;a href="https://github.com/estebanrfp/gdb/blob/main/LICENSE" rel="noopener noreferrer"&gt;free for personal and commercial use&lt;/a&gt;, but the source is proprietary by deliberate governance choice — it is not open source, and describing it as such would be inaccurate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content addressing is the feature.&lt;/strong&gt; If deduplication and permanent addressing of blobs is what you are buying, that is IPFS's job and it does it well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GenosDB is the better fit when the IPFS layer is a cost rather than a feature: when you want relational queries over a graph, roles and per-node permissions without a server, a two-megabyte dependency-free bundle, and an app that works with nothing deployed behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Where This Comes From
&lt;/h2&gt;

&lt;p&gt;I have spent years building browser P2P databases — I wrote the &lt;a href="https://gun.eco/docs" rel="noopener noreferrer"&gt;official GUN documentation platform&lt;/a&gt; and contributed to that codebase before starting GenosDB. Every design decision described here came from hitting these specific walls in real projects: queries I could not express, clocks I could not trust, permissions I could not enforce without a server, and bundles too heavy to justify.&lt;/p&gt;

&lt;p&gt;None of this is a knock on OrbitDB's ambition. It asked the right question in 2015 and answered it with the tools available then. GenosDB is what happens when you ask the same question after OPFS, WebRTC, WebAuthn and Nostr exist, and conclude that the storage layer does not have to be someone else's network.&lt;/p&gt;

&lt;p&gt;If anything here is out of date or wrong, tell me and I will correct it — &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; is open and I answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://genosdb.com/orbitdb" rel="noopener noreferrer"&gt;genosdb.com/orbitdb&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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>orbitdb</category>
      <category>ipfs</category>
      <category>database</category>
      <category>javascript</category>
    </item>
    <item>
      <title>RxDB vs GenosDB: Local-First With a Server, or Peer-First Without One</title>
      <dc:creator>Esteban Fuster Pozzi</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:44:27 +0000</pubDate>
      <link>https://dev.to/estebanrfp/rxdb-vs-genosdb-local-first-with-a-server-or-peer-first-without-one-jk1</link>
      <guid>https://dev.to/estebanrfp/rxdb-vs-genosdb-local-first-with-a-server-or-peer-first-without-one-jk1</guid>
      <description>&lt;p&gt;&lt;strong&gt;RxDB&lt;/strong&gt; is one of the best-engineered local-first databases in JavaScript. It has 23,000 stars, 270,000 downloads a month, and a commit history that does not go quiet. If you are building an offline-capable app that syncs with a backend you already run, it is a genuinely good answer, and this article is not going to pretend otherwise.&lt;/p&gt;

&lt;p&gt;But RxDB and GenosDB solve different problems, and the difference is not a feature list — it is where the authority lives. RxDB is a &lt;strong&gt;local database that replicates to a server&lt;/strong&gt;. GenosDB is a &lt;strong&gt;peer-to-peer database with no server at all&lt;/strong&gt;. Everything else in this comparison follows from that single line, including the parts where RxDB wins.&lt;/p&gt;

&lt;p&gt;This is an honest walkthrough of what RxDB is, where its own documentation draws the line, and what a peer-first design does differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is RxDB?
&lt;/h2&gt;

&lt;p&gt;RxDB (Reactive Database) is a local-first NoSQL database for JavaScript. It runs in the browser, Node.js, Electron and React Native, persists through a pluggable storage layer, and exposes documents and queries as RxJS observables. Queries follow MongoDB syntax and documents validate against JSON Schema.&lt;/p&gt;

&lt;p&gt;Its real strength is the replication layer. RxDB ships official plugins for HTTP, GraphQL, CouchDB, Firestore, Supabase, Appwrite, MongoDB, NATS, WebSocket and WebRTC. If your data already lives somewhere, RxDB probably has a plugin for it.&lt;/p&gt;

&lt;p&gt;It is also honest about itself, which is rare. RxDB publishes a page called &lt;a href="https://rxdb.info/downsides-of-offline-first.html" rel="noopener noreferrer"&gt;&lt;em&gt;Downsides of Local First / Offline First&lt;/em&gt;&lt;/a&gt; listing its own limitations, including "It only works with small datasets", "Browser storage is not really persistent" and "Realtime is a lie". That page is worth reading regardless of which database you pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Question
&lt;/h2&gt;

&lt;p&gt;Both databases store data in the browser and both can sync between clients. The difference shows up when you ask a simple question: &lt;strong&gt;when two peers disagree, who decides?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In RxDB, the server decides. Replication is a protocol between a client and an endpoint — &lt;code&gt;pull&lt;/code&gt; and &lt;code&gt;push&lt;/code&gt; handlers talking to something you operate. Conflict handling, authorization and identity are your backend's job, because that is where the trust lives.&lt;/p&gt;

&lt;p&gt;In GenosDB there is no such place. Every operation is signed by its author before it leaves the browser, and every peer verifies it independently against a shared set of rules. Authority is not a server — it is a signature. That is the Zero-Trust model, and it is what makes running with no backend at all a coherent design rather than a missing feature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RxDB                          GenosDB

Browser ──► RxStorage         Browser ──► Security Manager (signs)
   │                             │
   └──► replication plugin       └──► Graph Store (OPFS)
            │                            │
            ▼                            ├──► Peers (GenosRTC / WebRTC)
        Your backend                     │
        (authority)                      └──► Nostr relays (discovery only)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where RxDB Draws the Line — In Its Own Words
&lt;/h2&gt;

&lt;p&gt;RxDB does have a WebRTC replication plugin, so "RxDB can do P2P too" is technically true. The interesting part is what their documentation says about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  You must operate a signaling server
&lt;/h3&gt;

&lt;p&gt;From the &lt;a href="https://rxdb.info/replication-webrtc.html" rel="noopener noreferrer"&gt;official WebRTC replication docs&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"RxDB ships with a default signaling server (…) This server is made for demonstration purposes and tryouts. &lt;strong&gt;It is not reliable and might be offline at any time. In production you must always use your own signaling server instead!&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the serverless setup has a server in it. You deploy it, you keep it up, you pay for it, and you add authentication to it — their docs say as much: &lt;em&gt;"in production, you'd want to create a more robust signaling server with authentication and additional logic"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;GenosDB does not have a signaling server to operate, because peer discovery runs over the &lt;strong&gt;Nostr relay network&lt;/strong&gt; — public infrastructure that already exists, is already decentralized, and never sees your data. If you want your own, &lt;a href="https://github.com/estebanrfp/GenosSIG" rel="noopener noreferrer"&gt;GenosSIG&lt;/a&gt; runs an ephemeral relay on Cloudflare's free plan. Neither option is a server you maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  It is not a full mesh, and they say so
&lt;/h3&gt;

&lt;p&gt;Also from their docs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Notice that the peers still have to find each other through a signaling server, &lt;strong&gt;so this is not a full offline mesh.&lt;/strong&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is followed by a pointer to a &lt;em&gt;different product&lt;/em&gt; (Ditto) for environments with no infrastructure at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  OPFS and encryption are paid features
&lt;/h3&gt;

&lt;p&gt;This is the part most comparisons miss. RxDB's core is Apache 2.0 and free. But on the &lt;a href="https://rxdb.info/premium/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;, the free tier is capped at &lt;strong&gt;13 open collections in parallel&lt;/strong&gt;, and these sit behind &lt;strong&gt;Pro, from $99/month billed annually&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RxDB Pro feature&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RxStorage &lt;strong&gt;OPFS&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Default storage, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebCrypto Encryption&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built in, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RxStorage IndexedDB&lt;/td&gt;
&lt;td&gt;— (OPFS by default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fulltext search&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;$text&lt;/code&gt; operator, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worker / Sharding &lt;em&gt;(Pro Plus, $239/mo)&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Worker architecture, free&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OPFS — the storage engine that makes browser persistence fast — costs &lt;strong&gt;$1,188 a year&lt;/strong&gt; in RxDB. In GenosDB it is what you get when you run &lt;code&gt;npm i genosdb&lt;/code&gt;, because &lt;a href="https://github.com/estebanrfp/gdb/blob/main/LICENSE" rel="noopener noreferrer"&gt;the production bundle is free for personal and commercial use&lt;/a&gt;, with no tiers, no collection caps and no feature gates. The source is proprietary, which is a deliberate governance choice and is stated plainly — GenosDB is not open source, and calling it that would be inaccurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissions and authentication
&lt;/h3&gt;

&lt;p&gt;RxDB lists this among its own downsides, and the reasoning is sound: in a local-first app the client holds the data, so enforcing who may read or write it is not something the client can be trusted to do. Their answer is the backend.&lt;/p&gt;

&lt;p&gt;GenosDB's answer is cryptography. The &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-architecture.md" rel="noopener noreferrer"&gt;Security Manager&lt;/a&gt; provides WebAuthn-based authentication, &lt;a href="https://genosdb.com/genosdb-rbac-access-control" rel="noopener noreferrer"&gt;role-based access control&lt;/a&gt;, &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/sm-acls-module.md" rel="noopener noreferrer"&gt;ACLs at node level&lt;/a&gt; and &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/governance.md" rel="noopener noreferrer"&gt;rule-based governance&lt;/a&gt; for promotion and demotion. A peer that receives an unsigned or unauthorized operation rejects it — not because a server said no, but because the signature does not check out.&lt;/p&gt;

&lt;p&gt;This is the one axis where the difference is not a trade-off but a genuine capability gap, and it is why GenosDB can claim zero-trust and RxDB does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Peer-First Buys You
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cellular Mesh instead of full mesh
&lt;/h3&gt;

&lt;p&gt;A naive WebRTC mesh opens a connection between every pair of peers: n(n−1)/2 links, which collapses somewhere around 50–100 peers. &lt;a href="https://genosdb.com/genosdb-cellular-mesh-p2p-scalability" rel="noopener noreferrer"&gt;Cellular Mesh&lt;/a&gt; partitions peers into cells with bridges between them, turning quadratic growth into something that scales linearly. This is handled by &lt;a href="https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-api-reference.md" rel="noopener noreferrer"&gt;GenosRTC&lt;/a&gt;, GenosDB's own networking layer, with epoch-sealed topology so cell membership cannot be gamed mid-flight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid Logical Clocks instead of wall clocks
&lt;/h3&gt;

&lt;p&gt;Conflict resolution based on &lt;code&gt;Date.now()&lt;/code&gt; breaks when device clocks drift, which they always do. GenosDB uses &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; — causality without requiring synchronized time.&lt;/p&gt;

&lt;h3&gt;
  
  
  An operation log, so sync sends deltas
&lt;/h3&gt;

&lt;p&gt;Every write goes through an operation log, so a device that reconnects pulls only what it missed instead of a full state transfer. Measured in a user's app: a device rejoining a 210-item space pulls &lt;strong&gt;7.9 KB&lt;/strong&gt;. The same reconciliation without deltas costs 83 KB — more than ten times the traffic, on every single reconnection. The mechanism is documented in 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;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption that does not re-derive a key per node
&lt;/h3&gt;

&lt;p&gt;The standard way to encrypt data client-side is to derive an AES key with PBKDF2 — typically 100,000 iterations. It is what the Web Crypto guides recommend, it is cryptographically correct, and it costs roughly 22 ms &lt;strong&gt;every time it runs&lt;/strong&gt;. Because that derivation happens once per record, on every read and every write, the cost scales with the size of the list you are rendering.&lt;/p&gt;

&lt;p&gt;GenosDB derives nothing at read time. Each record carries its own key, already wrapped for the reader, so a read is one ECDH plus an AES decrypt. Same machine, same data:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Standard per-record PBKDF2&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read, per node&lt;/td&gt;
&lt;td&gt;10.9 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.2 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write, per node&lt;/td&gt;
&lt;td&gt;11.1 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.5 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading 40 encrypted records&lt;/td&gt;
&lt;td&gt;~1,790 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~50 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the difference between a list that renders instantly and one that stalls for nearly two seconds — same guarantees, same hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero runtime dependencies
&lt;/h3&gt;

&lt;p&gt;GenosDB ships with none. No transitive tree, no supply-chain surface to audit, nothing to patch when an upstream package is abandoned. That is not a small claim in 2026 — see &lt;a href="https://genosdb.com/gundb-supply-chain-security" rel="noopener noreferrer"&gt;GunDB's supply chain&lt;/a&gt; for what the alternative looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by Side
&lt;/h2&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;RxDB&lt;/th&gt;
&lt;th&gt;GenosDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Local-first, replicates to a backend&lt;/td&gt;
&lt;td&gt;Peer-to-peer, no backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data model&lt;/td&gt;
&lt;td&gt;Documents + JSON Schema&lt;/td&gt;
&lt;td&gt;Graph, schemaless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority&lt;/td&gt;
&lt;td&gt;Your server&lt;/td&gt;
&lt;td&gt;Cryptographic signature per operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signaling&lt;/td&gt;
&lt;td&gt;Your own server, required in production&lt;/td&gt;
&lt;td&gt;Nostr relays (public) or GenosSIG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2P topology&lt;/td&gt;
&lt;td&gt;Full mesh via plugin&lt;/td&gt;
&lt;td&gt;Cellular Mesh, native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflict resolution&lt;/td&gt;
&lt;td&gt;Custom handlers&lt;/td&gt;
&lt;td&gt;Hybrid Logical Clocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Dexie free · &lt;strong&gt;OPFS from $99/mo&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;OPFS, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pro tier&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built in, free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access control&lt;/td&gt;
&lt;td&gt;Your backend&lt;/td&gt;
&lt;td&gt;RBAC + ACLs + governance, in the client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collections (free tier)&lt;/td&gt;
&lt;td&gt;13 in parallel&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime dependencies&lt;/td&gt;
&lt;td&gt;Several&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;Apache 2.0 core, commercial plugins&lt;/td&gt;
&lt;td&gt;Proprietary source, free bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema migrations&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Same App, Both Ways
&lt;/h2&gt;

&lt;p&gt;A shared list that syncs live between browsers. Nothing exotic — the "hello world" of collaborative apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RxDB&lt;/strong&gt;, following their own documentation:&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;createRxDatabase&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="s1"&gt;rxdb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getRxStorageDexie&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="s1"&gt;rxdb/plugins/storage-dexie&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;replicateWebRTC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getConnectionHandlerSimplePeer&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="s1"&gt;rxdb/plugins/replication-webrtc&lt;/span&gt;&lt;span class="dl"&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;createRxDatabase&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="s1"&gt;notes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getRxStorageDexie&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// OPFS storage is a Pro feature&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;addCollections&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;version&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="na"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&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;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&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;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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="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;string&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="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;number&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;required&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="s1"&gt;id&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="s1"&gt;text&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;replicateWebRTC&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&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;notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-app-notes-room&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;connectionHandlerCreator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getConnectionHandlerSimplePeer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// "You can use the server provided by RxDB for tryouts,&lt;/span&gt;
    &lt;span class="c1"&gt;//  but in production you should use your own server instead."&lt;/span&gt;
    &lt;span class="na"&gt;signalingServerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss://signaling.rxdb.info/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="na"&gt;push&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;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebRTC Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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="s1"&gt;hello&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="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&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;GenosDB:&lt;/strong&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="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="s1"&gt;genosdb&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-app-notes-room&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="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;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&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="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;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="nf"&gt;render&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No schema to declare, no migration path to maintain, no storage plugin to license, and no signaling server to deploy — peer discovery is already handled over Nostr relays.&lt;/p&gt;

&lt;p&gt;To be fair to RxDB: that schema is not only ceremony. It buys validation, typed documents and a migration system, and if your data has a fixed shape enforced across a team, you may want exactly that. The question is whether you are paying for it in a project that does not need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Looks Like Running
&lt;/h2&gt;

&lt;p&gt;The claim "no backend" is easy to make and easy to check. Every one of these runs entirely in the browser, with nothing deployed behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collaborative editing without a CRDT library&lt;/strong&gt; — a block editor where two browsers edit the same document live:&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%2Fjf769nwbuml6ff44gqy3.gif" 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%2Fjf769nwbuml6ff44gqy3.gif" alt="GenosDB collaborative block editor syncing between two browsers" width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dCode — a code editor with branches, commits and pull requests, peer-to-peer:&lt;/strong&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%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, a peer-to-peer collaborative code editor built on GenosDB" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Hacker News clone where moderation is a signed constitution rather than an admin panel:&lt;/strong&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%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="A serverless Hacker News clone built with GenosDB" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A collaborative whiteboard, in a single file:&lt;/strong&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%2Fbg30knr1uyz2b10pkswv.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%2Fbg30knr1uyz2b10pkswv.png" alt="P2P collaborative whiteboard built with GenosDB" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are &lt;a href="https://genosdb.com" rel="noopener noreferrer"&gt;more of them&lt;/a&gt; — kanban boards, expense splitters, chat, file transfer, audio and video streaming, a Godot multiplayer plugin, and a WebGPU planet with no backend. The point of publishing so many is that "serverless" should be demonstrable, not asserted.&lt;/p&gt;

&lt;h2&gt;
  
  
  When RxDB Is the Right Choice
&lt;/h2&gt;

&lt;p&gt;I would rather be useful than win an argument, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You already have a backend.&lt;/strong&gt; If Postgres, Supabase, Firestore or CouchDB is your source of truth and you want offline capability on top, RxDB is built exactly for that and GenosDB is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need schemas and validation enforced.&lt;/strong&gt; RxDB's JSON Schema layer catches bad data at the boundary. GenosDB is schemaless by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need server-enforced authorization.&lt;/strong&gt; GenosDB verifies cryptographically at every peer, but there is no central authority to appeal to. For some compliance requirements, that is the wrong shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your dataset is too large for a full replica per browser.&lt;/strong&gt; Both have this limit; RxDB's storage tiers give you more room to negotiate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want an open-source core.&lt;/strong&gt; RxDB's is Apache 2.0. GenosDB's source is not public, and no amount of free licensing changes that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in return, GenosDB is the better fit when there is genuinely nothing behind the app: real-time collaboration with no backend to run, offline-first apps where each device holds a full replica, multiplayer browser games, or any product whose cost model cannot survive a per-seat sync bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Where This Comes From
&lt;/h2&gt;

&lt;p&gt;I have spent years inside browser P2P databases. I built the &lt;a href="https://gun.eco/docs" rel="noopener noreferrer"&gt;official GUN documentation platform&lt;/a&gt; and contributed to that codebase before starting GenosDB, and the design decisions described here — OPFS instead of localStorage, HLC instead of wall clocks, an oplog for delta sync, signatures instead of a trusted server — all came from hitting those specific walls in real projects, not from a whiteboard.&lt;/p&gt;

&lt;p&gt;RxDB is not the enemy in that story. It is a well-run project solving a different problem, by a maintainer who documents his own limitations in public, which I respect and have tried to match here. If anything in this comparison is out of date or wrong, tell me and I will correct it — &lt;a href="https://github.com/estebanrfp/gdb/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt; is open and I answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://genosdb.com/rxdb" rel="noopener noreferrer"&gt;genosdb.com/rxdb&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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>rxdb</category>
      <category>p2p</category>
      <category>database</category>
      <category>javascript</category>
    </item>
    <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>
  </channel>
</rss>
