Our codebase has about a hundred JavaScript files and 96 Jade templates. Around 7,500 lines of server-side code. 352 commits – 228 mine, 123 Jyoti’s. The readme is two lines. The todo list has two items – one about image upload paths, one about a bodyParser() deprecation we never got around to fixing.
This is what a web application looks like when two people build the whole thing.
The split
I own backend, database, and the graph model. Jyoti owns frontend – views, templates, client-side interactions. There’s overlap in the middle. Routes are mine. The Jade templates that render those routes are hers. The res.locals object is our contract – I populate it with data from the middleware chain, she reads it in the templates. We rarely step on each other’s code.
It wasn’t planned that way. It happened because we started from opposite ends and met in the middle. I was building the Neo4j model and the Express routes. She was building the layout and the pages. The middleware pipeline became the interface between us – I put things on res.locals, she takes them off.
What two people can’t do
We can’t specialize. There’s no dedicated person for infrastructure, security, testing, or deployment. I added helmet for security headers because I read about it. I added the LRU cache because queries were slow. Jyoti added client-side form validation because users were submitting empty plans. Each of us does whatever needs doing next, and most of what needs doing next is whatever broke last.
We don’t have tests. I know. The test directory exists. It has some files. They don’t cover much. When I write a model function, I put test code in an if (require.main === module) block at the bottom of the file. It runs when you execute the file directly. It’s not a test suite – it’s a sanity check. But it means every model file is also its own test harness, and I can verify a function by running node models/user/user.js without setting up anything.
We don’t have CI. Deployment is git push and pray. The production config has hardcoded values. The sample config has actual secrets in it because we never got around to separating those out. None of this is good. All of it is what happens when two people are trying to ship a product.
What two people can do
We can hold the entire codebase in our heads. Both of us. I know where Jyoti’s template partials live. She knows where my graph queries are. When something breaks, we don’t need to trace through documentation or ask a team – we know which file it’s in because we wrote it or we reviewed it.
We can change anything, instantly. No pull request process, no code review queue, no deployment pipeline. See a bug, fix it, push it. This is terrifying and fast. The speed is real. The terror is also real – every push is a small act of faith.
We can keep one pattern everywhere. The middleware chain works because there’s nobody arguing for a different approach. The graph model works because there’s nobody pushing for a different database. Two people don’t have enough surface area for disagreement.
The shape of it
The package.json tells the story. Express 3, Jade 0, Neo4j 0, Redis 0. Major version zero on most dependencies. We pinned nothing. The engine runs on Node 0.10. jQuery on the client side. Bootstrap for layout. No build step, no bundler, no transpiler.
This isn’t a stack anyone would choose today. It’s a stack that was available, that we knew, and that worked well enough to ship with. Every hour spent evaluating tools was an hour not spent building features. We picked what we already understood and started writing.
The lib/zutils.js file has a hand-rolled UID generator that checks for collisions against an in-memory cache. I wrote it because I needed unique IDs and didn’t want to think about it longer than twenty minutes. It works. The test data at the bottom of the file has vm, age: 27 and jc, age: 21. That’s from when we started. We’re both older now. The code isn’t.
When it’s enough
A two-person codebase is not a small version of a big codebase. It’s a different kind of object. A big team fails from coordination problems. A small team fails from exhaustion.
I don’t know yet whether this one will make it. The product needs users and we’re still figuring that out. But whatever happens, the code is honest. It looks exactly like what it is.