The rule was enforced. The copy wasn't.
We wrote a test that stops our pricing page advertising features we haven't built. It worked. Then a second, editable copy of that data drifted for five days on the page most people see.
We have a test that refuses to let our pricing page advertise a feature we haven't built. It works. Then the homepage rendered a copy of that data out of our CMS, and the copy drifted — telling people they got 30 products when the real limit was 50, and selling a feature we'd already shipped as something you'd get later.
The stale string is not the interesting part. The interesting part is that we'd already identified this exact risk, written a mitigation for it, and the mitigation was the thing that failed.
What was the guard, and why did we build it?
Our plan catalogue is one file. It has a rule written at the top of it, in capitals, because we earned it:
HARD RULE: every string here must map to code you can point at. A string in this file is not an implementation. Grep before you add one — five features were sold on the $59 Growth tier for months while existing only here.
That happened. Five capabilities were listed on a paid tier, and if you'd gone looking for the code behind them you'd have found nothing. Not malice — someone wrote the marketing copy first, intending to build it, and the intention decayed while the string stayed.
Prose in a contributing guide didn't prevent it. So we wrote a test instead: a list of capabilities with no implementation anywhere in the repo, and an assertion that none of them appear in any tier's included-features list. Adding one to the sold list now fails the build. To move something off that list you have to grep for it, name the module that implements it, and delete the line in the same commit.
That test has done its job for weeks.
So what went wrong?
The homepage didn't read the catalogue. It read a copy.
The three pricing cards on our front page came out of a CMS global — a stored, editable copy of the same tier data, seeded from the catalogue and then living its own life. And it drifted, in three ways at once:
- Free advertised "Up to 30 products". The real cap had been 50 for five days.
- Starter advertised "Up to 30 products". The real cap was 100. That tier is partly sold on its ceiling, and it was understating itself by seventy products.
- Starter still listed "Niblr branding removed" under In build, not yet available, with the line "subscribe now and you get them at this price when they land" — for something that had shipped the day before.
The one that should have been embarrassing: our own changelog was announcing "Free now holds 50 products, up from 30" on the same domain, two scrolls from a pricing card that said 30.
Why didn't anyone notice?
Because everything that could have noticed was pointed at the other copy.
The test guards the catalogue. The catalogue was correct the whole time — caps derived from the same constants the server actually enforces. /pricing, which reads the catalogue directly, was right. Every check we had said fine, and every check we had was looking at the file that wasn't rendering on the homepage.
There was no test that could fail here, because there was nothing to test against. A stored string in a database isn't wrong in any way a test can detect. It's just a string. It only becomes wrong relative to a source of truth it's supposed to match, and once you have two of those you don't have one.
We already knew. That's the uncomfortable part.
This wasn't an unknown risk. Somewhere in our scripts folder was this, written months ago by someone who understood the problem completely:
THIS IS A REQUIRED DEPLOY STEP when a feature string changes. Removing a phantom feature from
plans.tsdoes not remove it from an environment whose global is already populated: the stored copy keeps rendering until this runs.
A sync script. Push the catalogue into the CMS copy, per environment, every time a string changes. Correct diagnosis, working code, accurate warning in capital letters.
It didn't get run. Not through carelessness — the change that moved the product caps was about plan limits, and nothing in that work looked like "marketing copy", so nothing prompted anyone to think about a CMS sync. The step is invisible at exactly the moment you need to remember it.
That's the actual lesson, and it's not "be more careful." A required manual step is a step you will eventually skip. If your safety depends on a person remembering a thing that isn't in front of them, you've written a reminder, not a guarantee.
What we changed
We deleted the copy.
The homepage now projects its cards from the catalogue, the same way the pricing page always did. The CMS kept exactly two fields — the section heading and its subheading, which are genuinely editorial and genuinely nobody's source of truth. Then we dropped the stored plan array and its tables, and deleted the sync script along with the deploy step it required.
The point isn't that the copy is now correct. It's that it can't be incorrect, because it doesn't exist. The homepage inherits the catalogue's guarantee by construction: a feature string can't reach the front page without passing the test that says it has to map to real code.
We checked that the new test actually bites, rather than trusting a green run — put the old "stored copy wins" behaviour back, watched the test fail, put it back again. A guard you haven't seen fail is a guess.
What I'd take from it
Most advice about this is "make your CMS the single source of truth." I'd put it differently after this week: decide which data is editorial and which is a contract, and don't let the second kind be editable at all.
Our tier features are a contract. They say what you get for money. That data has exactly one correct value, it lives next to the code that enforces it, and the useful property is not that it's easy to update — it's that it can't disagree with what the software does. Putting it behind a CMS field made it easy to change and impossible to trust.
The heading above those cards is editorial. Someone should be able to reword it on a Tuesday without a deploy. That one belongs in the CMS, and it's still there.
The tell, if you want one: we'd written a script whose job was to keep two things in agreement. That script was the smell. Anything whose entire purpose is reconciling two copies of the same fact is evidence you should have one.
This is the second of these I've written. The first was about four bugs that shipped with passing tests, which turns out to be the same shape of problem from the other end: there, the tests ran and asserted the wrong thing. Here, the test asserted exactly the right thing and wasn't pointed at what shipped.