I wrote the first version of servo. It's in TypeScript, runs on Bun, and has been sitting on my laptop routing side projects for a while now. CLN later built his own implementation in Python, same shape, same contract, completely different code. This post is about why that turned out to be more useful than sharing mine.
The name is mine too. I wanted something that evoked the small, quiet machinery that just moves things where they need to go, not a framework, not a platform, something that serves. "Servo" fit. It's also short enough to type a hundred times a day without noticing.
Shared design, separate code
When CLN saw what I was running, the obvious move was to hand him the repo. We did the less obvious thing instead: I wrote down the shape of the tool, what a "servo app" is, what gets injected, what the routing rules are, what the config file looks like, and he built his own implementation from those notes.
The shared artefacts are plain: a handful of markdown files and some whiteboard photos. What a servo.json looks like. What the theme variables are and how they're injected. The rule that every directory under ~/servo/ becomes a URL path. How a handle() function gets called.
Everything else is up to the implementation.
Why not just share my code
Handing over the repo would have been less work for both of us. It would also have meant CLN inheriting every assumption I'd baked into Bun-shaped thinking, idioms, package layout, debugger, blind spots. Instead we got:
- Two perspectives on the same problem. I hit issues in Bun's HTTP parser that CLN never sees in Python's
http.server, and vice versa. Each surface teaches us something about the interface that neither of us would have noticed alone. - A built-in spec review. When CLN couldn't reproduce a behaviour from the notes alone, the notes were incomplete. That's a useful signal we'd never get with a shared repo: in a single codebase the implementation is the spec, and design intent disappears into the code.
- Freedom to diverge where it matters. My version leans into Bun-specific things, a native file watcher, streaming responses, a TypeScript config file I later walked back. His leans into Python-specific things,
importlibmodule reloading, zero dependencies, a single readable file. Neither of these would survive a merge.
What the shape looks like in Bun
The routing loop is short enough to fit in a paragraph:
// bun-servo: minimal routing
const server = Bun.serve({
port: 8000,
async fetch(req) {
const url = new URL(req.url);
const [, app, ...rest] = url.pathname.split("/");
const dir = path.join(APPS_ROOT, app);
const cfg = await loadConfig(dir); // servo.json
if (cfg?.type === "api") {
return callHandler(dir, req); // server.ts / server.js
}
return serveStatic(dir, rest.join("/")); // inject theme into HTML
}
});
The theme injection is a string insertion right before </head>, same CSS variables CLN ended up using, same <servo-toolbar> component contract. Apps written against one of our servos run unchanged on the other. That's the line we both hold.
Where the implementations have drifted
A few cases that turned out to be more interesting than a shared codebase would have allowed:
- Hot reload. Mine rebuilds from a native filesystem watcher; his reloads the Python module in place with
importlib. Observable behaviour is the same, edit a file, refresh, see the change, but the failure modes are different. We've each found bugs the other hadn't hit yet. - Config file. I flirted with
servo.tomlfor a week. Walked it back toservo.jsonwhen I realised apps were writing config files from scripts and JSON is the universal boring choice. CLN stayed with JSON the whole time and saved himself the detour. That's a decision you only make by trying the alternative. - Background tasks. Bun's worker primitives pushed me toward a long-lived task model for API apps. Python didn't push CLN the same way. Neither version has won yet, we'll see which shape survives contact with real use.
Freedom, not fragmentation
The thing I keep coming back to is how much experimentation this arrangement quietly enables. Sharing the spec means each of us is free to try things the other isn't convinced by. A single shared codebase would have forced every experiment through a consensus review; two implementations let us each commit to an idea long enough to learn whether it actually works.
When CLN couldn't reproduce a behaviour from the notes, the notes were wrong. A shared repo never tells you that; the code just is the spec.
Half of what ends up in the spec arrives that way: one of us tries something for a week, the other watches from a distance, and the parts that survive on both sides become the real contract. The parts that don't get quietly rolled back with a short note explaining why.
Maybe someday we write a servo to rule them all, a single canonical implementation that inherits the best of both. I don't rule it out. For now, though, developing on our own is the gift that keeps on giving. Every divergence is a small probe into whether the design was actually as tight as we thought. Most of the time it wasn't.
Notes, not meetings
The collaboration overhead is lower than you'd expect. We share architecture sketches in markdown and review each other's commits asynchronously. When something important shifts, a new app type, a change to the theme contract, whoever touches it first writes a short note. The other one implements from the note.
The whole thing is small enough that no heavier process is justified. If the tool gets big enough to need an RFC, it's probably the wrong tool.
The point isn't Bun
It would work just as well if the second implementation were in Go or Rust or OCaml. The value is in the separation, not the language. Two independent heads holding the same design in mind catch more things than one head holding one codebase, and when the implementations diverge, the divergence itself is information.
Servo is a small tool for routing side projects: every directory under ~/servo/ becomes a URL path, with a shared theme and toolbar injected into the HTML. There are two of them. Tavi wrote the first, in TypeScript on Bun. CLN wrote a second, in Python, from the same design notes. Apps run unchanged on either. The point of this post is why we kept them separate instead of sharing one codebase.
We share the spec, not the bits. The shared artefacts are a handful of markdown files and some whiteboard photos: what a servo.json looks like, what the theme variables are, the routing rule, how handle() gets called. Everything below that line is up to each implementation.
Why two and not one
Handing over the repo would have been less work. It would also have copied every Bun-shaped assumption into CLN's head. Two implementations buy three things instead:
- Two views of the interface. Bugs that show up in Bun's HTTP parser never appear in Python's
http.server, and the reverse. Each surface teaches us something about the contract. - A spec review for free. When CLN couldn't reproduce a behaviour from the notes, the notes were incomplete. A single codebase never tells you that, because the code is the spec.
- Room to diverge. Bun pushed Tavi toward a native watcher and streaming responses; Python gave CLN
importlibreloading and zero dependencies. Neither would survive a merge.
Every divergence is a probe into whether the design was as tight as we thought. Most of the time it wasn't.
Where they have drifted
Hot reload: Tavi's rebuilds from a filesystem watcher, CLN's reloads the Python module in place. Same observable behaviour, different failure modes. Config: Tavi tried servo.toml for a week and walked it back to JSON; CLN stayed on JSON and skipped the detour. You only learn that by trying the alternative. The parts that survive on both sides become the real contract; the rest gets rolled back with a short note.
The collaboration overhead is low. Markdown sketches, async commit review, a short note when something like a new app type changes. No RFC. If the tool ever needs one, it has grown too big. The value is the separation, not the language: it would work the same in Go or Rust. Two heads holding one design catch more than one head holding one codebase.