İçeriğe geç
wedevit

October 29, 2026 · 10 min read · product

İlhan Buğra Aslan

Every customer wants their own version: how to keep one product on one codebase


When a customer says "our approvals need two steps," the answer should land in one of three buckets: configuration stored as data, an entitlement granted by their plan or contract, or an extension point the product already exposes. The fourth option, a branch of the code just for them, is not a solution. It is a bill you agreed to pay later. Microsoft's architecture checklist for multitenant solutions names this outright as a deployment antipattern, listing "running separate versions of the solution for each tenant," "hard-coding tenant-specific configurations or logic," and "relying on manual deployments." Building the mechanisms is the easy half. The hard half is deciding, request by request, which bucket a demand belongs in.

"Custom work" is four different things

Four very different requests arrive wearing the same sentence, and they carry very different costs.

Branding and appearance. Logo, colors, a custom domain, the signature block at the bottom of an email. This is almost always configuration and should never involve writing code.

Business rule parameters. Approval thresholds, payment terms, shift start times, how many days before a reminder goes out. Also data, but not loose data: each one needs a type, a default, and a validation rule.

Integration endpoints. Connecting to the customer's own ERP, accounting system, or carrier. Neither a setting nor a rule, this is a separate mapping layer.

Genuinely new behavior. A screen or a calculation nobody else has asked for. This is the only bucket worth arguing about. If you do not already have a mechanism for the other three, every signed contract will open a new branch.

Keep settings in data, not in code

There is a quick test. If changing one customer's setting requires a deploy, the setting is in the wrong place. Tenant-specific values belong in a configuration store, not in a line that reads if (customerId == 42). The payoff is immediate: one version runs everywhere, support changes a value without opening a ticket with engineering, and nobody is surprised by that line during the next release.

Give every setting five attributes: key, type, default value, who is allowed to change it, and a validation rule. The default lives at the product level and the tenant value overrides it. Record who changed what and when, because three months later the only place that answers "why is this threshold seven days" will be your audit trail.

Entitlements and feature flags are not the same mechanism

Here is the mistake almost everyone makes: running pricing tiers through the feature flag system. The same Microsoft guidance warns against it directly. It notes that you sometimes need to enable specific features for specific customers, that this usually maps to pricing tiers, that feature flags are not usually the right choice for those scenarios, and that you should instead build a process to track and enforce the license entitlements each customer holds.

The reason is lifespan. The feature toggle taxonomy published on Martin Fowler's site and written by Pete Hodgson sorts toggles by how long they are meant to live. A release toggle is transitionary, its decision is static, and it gets deleted when the work ships. A permissioning toggle "may be very-long lived compared to other categories of Feature Toggles," and its decision is made per request. Store both in the same table and in two years nobody can tell which switch is safe to delete. Eventually someone cleans up a flag and turns off a feature a customer is paying for.

Separate them from day one. Flags have an owner and an expiry date and engineering removes them, which is the discipline we describe in the progressive rollout article. Entitlements have a contract, sales and support change them, and they tie back to billing. Two mechanisms, two screens, two owners.

Open extension points for everything else

Large platforms solved this years ago, and the solution is called an extension point. Shopify moved developers who wanted to customize checkout off editing the checkout template and onto app extensions. Its own documentation states the reasoning: the customization options are easy to install and upgrade-safe, which lets merchants keep benefiting when Shopify ships new products. The platform keeps control of the core structure and exposes only specific places where you are allowed to plug in.

SAP's "clean core" approach for S/4HANA is the enterprise version of the same idea, and its goal fits in one line: custom extensions do not break an upgrade, and upgrades do not break an extension. The condition is that extensions reach standard objects only through released, upgrade-stable interfaces. SAP also prescribes an order of preference, starting with what key-user tools can do (custom fields, custom logic, custom views) and moving to side-by-side extensions that run next to the core rather than inside it.

In a smaller product this is not expensive to copy. Custom fields, document and email templates, import and export, simple rule expressions, an API, and outbound webhooks cover most of it. One warning: every hook you open is a contract. The extension point you add this quarter becomes an interface you are obliged to honor, so the rules in versioning and backward compatibility apply here too.

Why a per-customer branch is the most expensive option

The cost of a fork is invisible on day one. The branch gets cut, the customer is happy, the deal closes. The bill starts arriving around month six. A critical security patch ships and you apply it n times. A dependency upgrade means resolving conflicts in every branch. A bug report comes in and the first question is always "which version is this customer on?"

The nastiest part comes later. A forked customer starts refusing upgrades, because every upgrade puts their special code at risk. You end up supporting the oldest version of your own product for years. The rule the WordPress community has repeated forever, edit a child theme instead of the parent, exists for exactly this reason: whatever you write over the top gets erased by the next update. None of this shows up as a line item, but it takes a cut of every sprint as technical debt.

Configuration is not free either

Do not overcorrect. Every new setting widens the surface you have to test. Ten independent on-off settings means 1,024 possible combinations, and no team tests that. Products that let settings multiply without limit eventually lose the ability to explain why something works in one account and fails in another.

Three rules keep it manageable. Every setting has a default, and the path you actually test is the default path. Every switchable feature gets a test for the off state too, because screens that break when something is disabled go unnoticed for months. Test profiles rather than combinations: define a handful of sample tenants derived from real customers and run your regression suite against those. That is where the test pyramid and coverage discussion turns concrete.

Accept one more thing. Adding a setting is a product decision. A setting with no name, no documentation, no default, and no way back is a trap you left for your support team.

Five questions that sort the request

When the next "can you do this just for us" arrives, work through these in order.

  • How many customers want it? One customer means an entitlement or an extension. Three or more means configuration. If most of them want it, you need to change the default behavior instead.
  • Does it sit beside the standard flow or inside it? Beside it, an extension point is enough. Inside it, this is a product decision that belongs on the roadmap.
  • Is it solved with data or with code? Most requests that boil down to "one more field" are really a data modeling question, best read alongside the mistakes in the data model article.
  • Can it be reversed if the customer leaves? Anything irreversible is permanent maintenance load.
  • Who maintains it in three years, and who is paying for that? A request with no answer here is identical to a request whose answer is "everyone."

Put an approval gate in place

The sharpest part of SAP's approach is governance rather than technology: extension routes are classified, some recommended, some allowed only when nothing else works and only with architectural approval, and some prohibited outright.

In a team of five the equivalent is not a committee, it is one name. Route every non-standard request through the product owner and write the decision down in a single paragraph: what was asked, which bucket it went into, why, and who owns the maintenance. The document does not matter much. What matters is that somebody has the authority to say no. Without that, your sales calls decide your architecture, and you will not notice for six months.

If you sell custom work, price it properly

Building customer-specific work is a legitimate business. Burying it in the product price is not. Keep three line items separate: the one-time build, the annual maintenance for that build, and an option to migrate onto the standard product later. Put the ownership of the code, the version line it will be supported on, and who carries upgrade responsibility into the contract. The risk split in fixed price versus time and materials and the clauses in a maintenance and support agreement set the ground for that conversation.

If you already have forks

A four-step exit works. Start with an inventory: how many branches, which customer each belongs to, which files they touch. Then diff them against the core and list the actual changes. Third, find the common denominator, because in practice most differences collapse into a few settings and a couple of templates. Last, move the genuine remainder onto an extension point and close the branch.

This is cleanup, not a rewrite. Do not try to close every branch at once. Start with the two that hurt most and measure how many hours the next upgrade saves you.

Three steps that fit in this week

One: search the last six months of commits for code written "just for this customer." Conditionals on a customer identifier and branches that diverged from the main line give you a solid starting list.

Two: if your feature flags and your paid entitlements live in the same place, split them. Which switch is a temporary engineering lever and which one is a feature someone pays for, you can produce that list today.

Three: answer the five questions in writing the next time a custom request lands, and store the decision somewhere. Three months from now that record is the only data showing which bucket actually worked.

At Wedevit we build the configuration layer that keeps a product on one codebase, separate license entitlements from feature flags, design extension points as versioned interfaces, and produce the plan that folds existing customer branches back into the core. A concrete first step: if you shipped a security patch today, could you say from a single screen how many separate versions you would have to apply it to?


Need help with this topic?

get in touch →← all posts