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