01 The problem

A hundred forks and the question “which one?”

Our product is a 3D tour widget that developers, property portals and agencies embed through an iframe. Nearly every B2B client wants their own look: colours, logo, buttons, language, where the minimap sits. The first design was sensible — the core in an npm package and a thin wrapper with a config file, forked per client.

It worked for five clients and fell apart by the hundredth. Each fork froze a version of the core; over four years the package shipped around 450 versions, so a bug fix became a campaign across dozens of repositories. Onboarding a partner took up to two weeks, and developers spent their time nudging elements and swapping hex codes. Worst of all, nobody could say which settings a given version supported — the answer lived in code, stale docs and one person's memory. The real problem was the absence of a configuration contract.

02 The decision

The type as source, the schema as an executable contract

We already had that contract: the TypeScript type for the widget config, with its unions, flags and TSDoc comments. The job was not to invent it but to carry it to the people who set clients up. On every release tag, CI generates a JSON Schema from the type — comments become descriptions, union types become enums.

Schemas go to S3, laid out by version with a release manifest pointing at the commit and pipeline. No database was needed, because published versions never change. One detail would otherwise have broken things silently: latest moves only when the tag matches the current npm release, so a hotfix to an old line cannot overwrite it.

In the admin panel the schema is wired into Monaco, the editor behind VS Code. It provides completion, inline documentation and error highlighting out of the box, and an invalid config cannot be saved. Next to it sit a schema version picker and an iframe running the real widget, which cut the edit loop from days to a couple of seconds. At runtime the widget fetches its branding by hostname, so a new client is a database row rather than a build: hundreds of subdomains are served by one wildcard config and four certificates.

03 The outcome

800+ branded releases, no developer involved

The system has run in production for three years and served more than 800 brandings, 20 to 45 new ones a month. Around a hundred repositories became one, partner onboarding dropped from two weeks to a couple of days, typo-driven misconfigurations disappeared, and about a third of engineering time went back to the product. Fewer than one per cent of clients still need custom JavaScript.

Usage data held a surprise. About two thirds of brandings never touch a parameter, and three keys account for 95% of the configurations that do. The most used one overrides interface text, not colours as we had assumed.

04 Takeaways

What to take away

Forks are the symptom; a missing contract is the disease. The source of that contract usually already exists in your types, and the work is carrying it all the way to the UI without creating a new chore — we stopped maintaining parameter documentation separately, because TSDoc now reaches the editor on its own.

And we would not have guessed which settings mattered most. Exposing the whole schema and deriving forms from it turned out safer than designing a polished screen around the ten options that looked important.