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.

shared design notes routing · theme · app types Tavi's servo (first) TypeScript on Bun original implementation CLN's servo Python, single file built from the notes
One spec, two implementations. We share the design, not the bits.

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:

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:

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.