Skip to main content

Command Palette

Search for a command to run...

A Hacker News Clone With No Server — Four Files, a Constitution Every Peer Runs, Just GenosDB

The front page you know, peer-to-peer: every vote is a node its author owns, and the rules that moderate it are a file in the repository, rendered on the site and enforced on every browser.

Updated
12 min readView as Markdown
A Hacker News Clone With No Server — Four Files, a Constitution Every Peer Runs, Just GenosDB
E

Full Stack Developer - dWEB R&D

Every Hacker News Clone Ends With a Server

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.

This one has no server. Four files, no build, no backend, no accounts. 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 constitution every peer runs 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 GenosDB, a graph database that lives in the browser, signs every write, and syncs between peers with nothing in the middle.

The front page of dNews: Hacker News's bar, ranks, points and threads, with no server behind it

  • Live: dNews — sign in with one click, submit, vote; open a second browser and watch it follow.
  • Source: estebanrfp/dNewsindex.html, styles.css, app.js (under 500 lines) and constitution.js.

The Whole Idea in Three Sentences

  1. The graph is the site, and every visitor holds it. There is no API to call: db.map is a live query over the nodes on your own disk, and the peers keep those nodes the same.
  2. Every node is signed and owned. 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.
  3. Nobody decides; everybody calculates. 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.

What a Shared Constitution Changes

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.

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:

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

Two things fall out of that which no amount of good will can give a moderated site. The rule cannot have a prejudice, because it cannot see who you are — only what your nodes say and what trusted members did about them. And power is bounded by construction: 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.

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

Step 1 — One Subscription, Every Page

const db = await gdb("dnews", {
  rtc: true,
  sm: { superAdmins: [CONSTITUTION.authority], customRoles: CONSTITUTION.roles, governanceRules, acls: true },
})

// One live query feeds a store; the front page, a thread, a profile are
// pure functions of it. Nothing is fetched, nothing ticks over the wire.
const nodes = new Map()
await db.map({ query: { \(or: [{ type: { \)in: ["story", "comment", "vote", "flag", "vouch"] } }, { role: { $exists: true } }] } },
  ({ id, value, timestamp, action }) => {
    if (action === "removed") nodes.delete(id)
    else nodes.set(id, { id, value, timestamp })
    scheduleRender()
  })

That is the whole data layer. gdb(name, { rtc: true }) opens a local database under that name and joins the room of the same name; the sm 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 $or pulls the user: nodes too, which is where the Security Manager keeps each identity's role.

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

Step 2 — A Vote Is a Node You Own

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:

// No id: with `owner` on the value the engine names the node `\({owner}:\){uuid}`,
// and every peer refuses a write or a deletion from anyone else.
const create = async (value) => {
  await db.sm.executeWithPermission("write")          // the engine's own verdict, before the write
  return db.sm.acls.set({ ...value, at: Date.now() })  // the creator becomes the owner
}

await create({ type: "vote", item: storyId, dir: 1 })
await create({ type: "comment", text, parent: storyId, story: storyId })

db.sm.acls.set writes the node with the signer as owner; 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 dir: 0 on your own node, never a deletion; the site keeps no history, but it never rewrites one either.

A thread on dNews: comments are nodes owned by their authors, nested by a parent field

Step 3 — The Constitution Is a File

Everything that decides who may do what is in constitution.js, and the site renders that file, verbatim, on its constitution page. Two kinds of rule live in it, and the difference is the point:

roles: {
  guest:      { can: ["read", "sync"] },                                  // where everyone starts
  restricted: { can: ["read", "sync"] },                                  // lost the right to write
  user:       { can: ["write", "link", "sync"], inherits: ["guest"] },    // posts, comments, votes, flags
  superadmin: { can: ["assignRole"], inherits: ["user"] },                // the authority: signs roles, nothing else
},
rules: [ // evaluated in order, the last match wins
  { if: { role: "guest" }, offsetTimestamp: 10_000,                      then: { assignRole: "user" } },
  { if: { role: { $in: ["user", "restricted"] } },                       then: { assignRole: "user" } },
  { if: { role: { \(in: ["user", "restricted"] }, karma: { \)lte: -10 } }, then: { assignRole: "restricted" } },
],
thresholds: { gravity: 1.8, vouchKarma: 10, vouchPenalty: 5, flagKarma: 5, flagsToKill: 3, downvoteKarma: 20, deadScore: -5 },

Roles and rules are enforced by the engine. 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.

Thresholds are derived by every peer. 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 showdead on. No moderator decided it, and anyone can recount it.

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.

The constitution page: the same file the app runs, rendered — roles, rules, thresholds, and how to amend them

Step 4 — Writing Is Free, Influence Is Earned

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:

// Trust is a chain of signed vouches that starts at the authority. A vote
// or a flag counts only from a trusted owner — a hundred fresh identities
// voting for each other add up to zero.
const trusted = new Set([AUTHORITY])
let grew = true
while (grew) {
  grew = false
  for (const v of vouches) {
    const from = v.value.owner.toLowerCase(), to = v.value.for.toLowerCase()
    if (trusted.has(from) && !trusted.has(to)) { trusted.add(to); grew = true }
  }
}
const countVote = (voter, dir) => trusted.has(voter) && (dir > 0 || upKarma.get(voter) >= T.downvoteKarma)

Anyone may post the moment the constitution makes them a user. But a vote or a flag counts 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.

Step 5 — Karma You Can Recount

The role rules read karma 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:

A profile on dNews: karma computed here beside the karma the authority certified, and who vouched for whom

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.

What It Does Not Do — and Why That Is the Design

  • Nobody edits or deletes anyone else's post. 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.
  • Two members writing the same node at once never happens — every node has one owner. Two members voting at once are two nodes, and every peer counts both.
  • A mnemonic session lives in memory 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.
  • The engine holds one copy per visitor. 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.

Run It

git clone https://github.com/estebanrfp/dNews && cd dNews
bun tests/server.mjs        # http://localhost:5705 — any static server will do

There is nothing to build: the engine arrives from a CDN with one import. ?room=anything opens a private sandbox of the whole site; the tests use it, and so can you. Three Playwright tests 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.

The examples catalogue has forty more pages built the same way, and the Security Manager guide and the governance guide cover every call used here.

⭐ Found this useful? Star GenosDB on GitHub — or spin it up in seconds: npm i genosdb.


This article is part of the official documentation of GenosDB (GDB).
GenosDB is a distributed, modular, peer-to-peer graph database built with a Zero-Trust Security Model, created by Esteban Fuster Pozzi (estebanrfp).

📄 Whitepaper | overview of GenosDB design and architecture

🛠 Roadmap | planned features and future updates

💡 Examples | code snippets and usage demos

📖 Documentation | full reference guide

🔍 API Reference | detailed API methods

📰 Articles | concepts, tutorials and use cases

💬 GitHub Discussions | community questions and feedback

🗂 Repository | Minified production-ready files

📦 Install via npm | quick setup instructions

🌐 Website | GitHub | LinkedIn