One Copy Per Customer
Every bespoke customization is a promise that somebody will hand-apply your next fix N times. Here is the test for when the difference belongs in a copy and when it belongs in a manifest.
By Jason Waldrip, with Claude 10:15
The customer asks for something small. One extra field on the portal, different copy on the second onboarding step, and could the logo sit on the other side. None of it is hard. The quickest way to say yes is to take what they already have, copy it, edit their copy, and ship it under their name. You say yes by Thursday. They tell their account manager you’re the most responsive vendor they’ve ever worked with, and they mean it.
That was probably the right call, priced the way you priced it. An afternoon of work bought a renewal and a reference. The problem is that you paid for it at write time, and the rest of the invoice comes due at read time, spread across every person who touches that code after you, for as long as the customer exists.
Nobody itemizes that part. It doesn’t show up as a line anywhere. It shows up eleven customers later as a Tuesday.
the fix you will apply N minus one times
Here’s the Tuesday. A real bug, in the shared part, the part that was shared right up until you started copying it. Auth, or a date rendering wrong for anyone east of London, or something in the upload path that eats files over a certain size.
You fix it. Then you go find the eleven other places it lives, and you fix it there, and each one has drifted a little because each one has its own year of small edits on top. Two of them need the fix reshaped. One of them can’t take it at all without unwinding a change someone made for a customer who churned in 2024.
Ask a support engineer at any company that grew this way and you’ll get the same tell: before they can answer a question they have to ask which customer, and then go look. Not to check a config value. To go read that customer’s code and find out what it does.
That’s the shape I care about. When “what does the product do” can only be answered per customer, by reading, you don’t have a product with customers. You have a portfolio.
the friction that was doing all the work
For twenty years this pattern was self-limiting, and not because anyone was disciplined about it. It was limited because copies were work. Someone had to sit down and produce customer number twelve’s version, and by around customer five that person was tired enough to walk into a room and argue for doing it properly.
Fatigue was the governor. It was a bad governor, it fired late and it fired at random, but it fired.
It doesn’t anymore. Point a fleet at an existing bundle and ask for a variant with those three changes and you’ll have it before the call ends, tested, documented, in a clean commit. The thing that used to make the fifth copy hurt has been removed, and the thing that makes the fiftieth copy expensive has not moved at all. Volume got cheap. Ownership didn’t.
So the pattern that used to cap out around a handful now runs until something else stops it, and I’ve been in enough of these to know what usually stops it: a security patch that has to reach everyone by Friday.
what you were actually selling
Sit with what the customer bought, because it isn’t what they think they bought.
They think they bought a bespoke portal. What they actually bought is a difference. One field, one label, one arrangement. The difference is small and it is the entire product from where they sit, and none of that requires the difference to live in a copy of your codebase. It requires the difference to live somewhere your system can read.
That’s the whole reframe, and it’s less about architecture than about where you put the variation. Copy the code and the variation lives in a diff nobody reads. Put it in configuration and the variation lives in a row you can query, audit, roll back, and hand to a support engineer who can answer the question without opening an editor.
I’m doing this right now at Phoneware. Portal features had grown up as per-customer JavaScript bundles, which is exactly the pattern above, arrived at honestly, one reasonable yes at a time. The replacement is one global bootstrap plus a manifest API that cascades reseller → domain → role, with a configurator on top so the cascade is something you set rather than something you deploy. A new customer is a manifest, not a branch. A feature that six customers want is one module, enabled six times.
The cascade is the part I’d defend hardest. Most customization isn’t per customer at all, it’s per tier, per reseller, per role, and a copy flattens all of that into one undifferentiated snowflake. Inheritance keeps the shape of the business visible in the shape of the config. When somebody asks why this customer sees that, the answer is a lookup and not an investigation.
the test
Not every fork is wrong. Some differences are real, and pretending otherwise gives you a config system with three hundred switches, which is a fork in a worse costume. Feature flags earn their keep the same way and carry the same failure: a flag you never delete is a permanent branch in your logic, which is what the flags page on this site calls the cleanup you don’t get to skip.
So I use one question, and it fits on a sticky note. Can you name the axis?
- If the difference varies along something you can name, it’s configuration. Plan tier, region, reseller, role, locale, brand. Named axes compose. Add the customer to the axis and the work is a row.
- If the difference is “because it’s them,” you have a services engagement. That can be a fine business, and I’ve watched good companies make real money on it. It just has to be priced and staffed as one, with the maintenance in the contract, instead of hiding inside a product margin that assumed one codebase.
- If a customer needs a genuinely different domain model, fork it on purpose. Different product, different repo, different roadmap, said out loud. A deliberate fork you’ve named is survivable. The dangerous fork is the one nobody ever decided to create.
- If you can’t tell, count. How many places would you have to edit to change the login screen for everyone? Say the number to your CEO. It’s the most honest architecture metric I know, and it takes about a minute to find.
The number is usually worse than the team guesses, and the gap between the guess and the number is the actual finding. It means the copies were being made faster than anyone was tracking, which was true when a person made them and is much more true now.
the next small ask
None of this makes the copy the villain. Copying was the right move on the day, with the information available, on a deadline that was real. I’ve made that call. Anyone who has shipped to enterprise customers has made that call, and the alternative on that particular Thursday was telling a customer no while a competitor told them yes.
What changed is the rate. You’re not making five of these decisions a year anymore, you’re making them at whatever speed your fleet can produce, and the cost of each one still lands years later on somebody who wasn’t in the room. That’s the part worth being deliberate about, because deliberate is now the only governor left.
The next customer is going to ask for something small. Saying yes isn’t the question. The question is whether your yes has somewhere to live besides a copy with their name on it.
Jason Waldrip is a fractional CTO and CAIO. He writes about engineering leadership, infrastructure, and building in the age of agents. If your product has quietly become one codebase per customer, that’s the work I do.
A note on how this was made: I wrote this with Claude. I brought the frame, the client work, and the calls on what stayed and what got cut; Claude did most of the drafting. Same honesty note as always: the tool was in the room, and saying so costs nothing.