# A GitHub With No Server — dCode, a Peer-to-Peer Code Editor and Code Host in One Page, Just GenosDB

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.

That is **[dCode](https://estebanrfp.github.io/dCode/)**: a real-time collaborative code editor and a GitHub-style code host in one page, for projects that are **one HTML file** — 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 [GenosDB](https://github.com/estebanrfp/gdb), a peer-to-peer graph database that lives in the browser.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-editor.png)

*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 `main`, and every row of it runs.*

This is the sequel to [Real-Time Collaborative Editing Without a CRDT](https://genosdb.com/genosdb-collaborative-editing), 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: **the things GitHub runs servers for**.

## What GitHub Runs Servers For

A code host is a list of guarantees, and each of them is usually a server's job.

| On GitHub, on CodePen | On dCode |
|---|---|
| A server holds the repositories, a server runs the collaboration | Every visitor holds the graph; peers sync it directly over WebRTC, live typing included |
| An account is a row in a database | An identity is a key pair on your device: a mnemonic recovers it, a passkey keeps it |
| A shared editor needs a CRDT library and a relay | A line is a node: two people on different lines never collide, the same line is last-write-wins, carets ride an ephemeral channel |
| Branch protection is a server rule | A branch is a node its owner owns: only the owner, and the addresses granted `write`, can move its head — refused on every peer otherwise |
| A commit is trusted because the server says so | 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 |
| A pull request's state is a column | 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 |
| History is browsed | History **runs**: a project is one HTML file, so any commit opens in a sandboxed frame as it was |
| Offline is an error | A commit made with no peer in sight waits on this device's disk and lands when a path exists, signed like any other |

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.

## Two Layers, One Graph

dCode keeps two kinds of state in one GenosDB graph, and the difference between them is the whole design.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-two-layers.svg)

*Left, the buffer: plain nodes anyone on the branch may write. Right, the history: nodes only their owner may write. One subscription feeds both.*

**The buffer is shared.** Every line of a branch's code is a plain node — `{ type: "line", branch, text, order }` — 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.

**The history is owned.** A repository, a branch, a commit and a pull request are nodes created with GenosDB's ACL layer, `db.sm.acls.set`. 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.

Both layers arrive through one subscription:

```javascript
await db.map({ query: { type: { $in: ["repo", "branch", "commit", "pr", "line"] } } }, ({ id, value, action }) => {
  if (action === "removed") nodes.delete(id); else nodes.set(id, { id, value })
  // lines go straight to the editor; everything else redraws the timeline, the branches, the pull requests
})
```

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.

## The Editor Is the Block Editor, for Code

The [block editor](https://estebanrfp.github.io/gdb/examples/block-editor.html) made a document out of paragraph nodes. dCode makes a file out of line nodes, and inherits every property that article argued for:

- **Line-level last-write-wins.** Two people on different lines never collide, because there is no shared node to fight over. Only two carets on the *same* line still race — and they can see each other, which is why they rarely do.
- **Fractional order keys.** A line inserted between keys `13` and `15` gets a key in the gap. Two peers inserting at the same place mint different keys, both survive, and every peer sorts them identically: since GenosDB 0.33.9 a sorted `db.map` 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 [Ordered Lists guide](https://github.com/estebanrfp/gdb/blob/main/docs/ordered-lists.md).
- **Awareness is ephemeral.** Carets, selections and the line being typed travel on a [GenosRTC](https://github.com/estebanrfp/gdb/blob/main/docs/genosrtc-guide.md) channel that never touches the database — which is also how Yjs does awareness. Keystrokes are coalesced to one message per frame; the debounced `put` remains the truth that persists, repairs and survives offline.

```javascript
// The truth: one node per line, written 250 ms after the last keystroke.
db.put({ type: "line", repo, branch, text, order }, lineId)

// The experience: the line being typed, keystroke by keystroke, on the room's channel.
db.room.channel("presence").send({ kind: "text", block: lineId, start, end, text })
```

Two things a code editor needs that a document did not: **line numbers**, which are positions derived from the order and never stored, and **syntax colours**, painted per line by the language the line is in — HTML, the CSS inside `<style>`, the JavaScript inside `<script>`. The same pass that finds those regions gives the **HTML · CSS · JS views**: filters over the one file, showing the `<style>` block or the `<script>` 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 `<style>` and stays visible there.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-views-css.png)

*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.*

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

## A Commit That Names Itself

A commit is a node holding the whole file, a message, its parents and the branch it was made on. What makes it *history* rather than data is its id.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-commit-id.svg)

*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.*

```javascript
const id = `${me}:${await sha256([repo, parents.join(","), message, content, at].join("\n"))}`
await db.sm.acls.set({ type: "commit", repo, branch, parents, message, content, at }, id)
```

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.

## A Branch Only Its Owner Moves

A branch is an owned node with a `head`. The owner moves it; the owner can grant `write` to collaborators with `db.sm.acls.grant`; 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.

What does someone without write do? They edit the shared buffer like anyone — and when they commit, the commit goes to **a branch of their own**, 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.

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 `main` has not moved.

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.

## Pull Requests Nobody Marks as Merged

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 *merged* 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.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-merge.svg)

*Three merges, one rule: the target's owner writes, every peer reads. The shared buffer follows the new head in all three.*

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.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-pull-request.png)

*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.*

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.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-conflict.png)

*Both changed the heading. The markers are in main's buffer for everyone to see; the next commit on main will be the merge.*

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-history.png)

*The timeline is a graph in the margin: one lane per branch, one row per commit, and every row runs.*

## Offline Is a Commit Like Any Other

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.

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.

## Private Repositories: The Engine Keeps the Key

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.

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

From there the application does the small part. Tick *Private* 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 **member** is an address the owner grants `read` 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.

![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.](https://cdn.jsdelivr.net/gh/estebanrfp/gdb@main/assets/dcode-private.png)

Two details are worth the space. First, **the code is opened in memory and never written back in clear**: 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, **the authority is not a member**: 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.

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.

## The Constitution: Writing Is Free, Nothing Owned Is Touchable

dCode is governed the way [dNews](https://genosdb.com/genosdb-hacker-news-clone-no-server) 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:

- **guest** — 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.
- **restricted** — lost the right to write. Its repositories, branches and commits stay exactly where they are, signed by it.
- **superadmin** — 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.

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 `delete`: 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 [zero-trust model](https://github.com/estebanrfp/gdb/blob/main/docs/zero-trust-security-model.md) 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.

## What It Deliberately Does Not Do

- **Character-level merging inside one line.** Two carets on the same line are last-write-wins, with the carets in view. That is the dividing line the [previous article](https://genosdb.com/genosdb-collaborative-editing) drew, and it holds for code: people rarely type inside the same line at the same instant, and when they do they see it.
- **Folders, binaries, git interoperability.** A project is one HTML file. That is what makes every commit runnable, and it is the size of project this is for.
- **Declining a pull request from the target side.** A pull request is the proposer's node; the target's owner ignores it, or merges it. The proposer withdraws it.

## Try It

The application is four files, with no build step and nothing to deploy:

- 👉 **[Open dCode](https://estebanrfp.github.io/dCode/)** — sign in as `alice` in one window and `bob` in another, create a repository, type in both, commit, fork, propose, merge. Tick *Private* 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.
- 📄 **[Read the source](https://github.com/estebanrfp/dCode)** — the model is documented in the README and in the constitution; the tests say what each claim rests on.
- 📝 **[Real-Time Collaborative Editing Without a CRDT](https://genosdb.com/genosdb-collaborative-editing)** — the technique the editor is built on, with the fractional keys and the tie rule explained.
- 🧭 See how GenosDB compares across [other P2P and distributed databases](https://genosdb.com/popular-p2p-distributed-databases), or read the [full GunDB guide](https://genosdb.com/gundb) if that is where you are coming from.

⭐ 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: **[dCode](https://github.com/estebanrfp/dCode)** for the application and **[GenosDB](https://github.com/estebanrfp/gdb)** for the engine that makes it possible. Or spin the engine 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](https://github.com/estebanrfp))**.

📄 [Whitepaper](https://github.com/estebanrfp/gdb/blob/main/WHITEPAPER.md) | overview of GenosDB design and architecture

🛠 [Roadmap](https://github.com/estebanrfp/gdb/blob/main/ROADMAP.md) | planned features and future updates

💡 [Examples](https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-examples.md) | code snippets and usage demos

📖 [Documentation](https://github.com/estebanrfp/gdb/blob/main/docs/index.md) | full reference guide

🔍 [API Reference](https://github.com/estebanrfp/gdb/blob/main/docs/genosdb-api-reference.md) | detailed API methods

💬 [GitHub Discussions](https://github.com/estebanrfp/gdb/discussions) | community questions and feedback

🗂 [Repository](https://github.com/estebanrfp/gdb) | Minified production-ready files

📦 [Install via npm](https://www.npmjs.com/package/genosdb) | quick setup instructions

🌐 [Website](https://estebanrfp.com/) | [GitHub](https://github.com/estebanrfp) | [LinkedIn](https://www.linkedin.com/in/estebanrfp/)

