Last week the legal identity behind Munchable changed. The app is the same app; the question of who you are contracting with when you use it got a different answer.
That sounds like a copy edit. It is not, and the interesting part is which parts of a codebase turn out to know the answer. In this repository it was eight files, 73 lines added and 107 removed, and two of those lines are the ones that matter.
You can read the result on munchable.app/about, in the footer of every page, and in the contracting party named at the top of munchable.app/terms and in the data controller clause of munchable.app/privacy. Those four places say the same name because they all read it from the same constant.
Identity belongs in data, not in prose
There is one file, lib/publisher.ts, hand-copied into five repositories. It holds the company, and the list of apps the company publishes:
export const PUBLISHER = {
name: 'CogniPrep Ltd',
registeredIn: 'England and Wales',
companyNumber: '17407266',
id: 'https://cogniprep.app/about#cogniprep-ltd',
} as const;
Everything that has to name the publisher reads from it. The terms page interpolates it into the sentence about who the agreement is with. The privacy page interpolates it into the data controller clause, with the company number. The footer's copyright line used to read © 2026 Munchable and now reads © {year} {PUBLISHER.name}. The transactional email footer, which previously carried a hand-written sentence about who runs the product, now carries the same derived line.
That is the design decision the rebrand tested: a legal identity is data, and the prose that mentions it should read the data rather than contain it. The test went well. Changing the name meant changing the constant, then fixing the handful of sentences whose grammar had been written around the previous answer, because "run by a person" and "published by a company" are not the same shape of sentence even when they are the same length.
The id field is worth a note: it is an identifier for the company in structured data, not a link we promise to keep working. Making it a URL that must resolve to a page would mean every family site is one deleted anchor away from emitting a broken reference.
The file is five copies, and the type system guards the one line that differs
Each repository's copy is identical apart from one line, which names the app that repository builds:
export const APPS = [ /* five entries */ ] as const satisfies readonly FamilyApp[];
export const SELF: AppKey = 'munchable';
as const satisfies rather than : readonly FamilyApp[]. With the annotation, every key widens to string, so AppKey becomes string, and a typo in SELF compiles cleanly. What you get at runtime is an app that fails to find itself in the list, which means the "other apps from the same publisher" section lists the app you are already looking at, on a page whose whole purpose is to establish who else is in the family.
That is the most likely mistake in a file whose copying instructions say "change exactly one line", so it is worth spending a type on. The comment in the file says so, because the next person to simplify the annotation will not otherwise know what it is load bearing for.
Structured data has to agree with the terms
Every family site emits an Organization node for itself with the publisher as its parentOrganization. The comment above that code is blunt about why the two must match:
Every app's Organization names the company as its
parentOrganization, the same company the terms and privacy notice name as the contracting party and data controller.
Before the change, the apps were linked in structured data by the individual who builds them rather than by a shared publisher, because that was what the terms said at the time. Had the structured data named a company the terms did not, it would have been an assertion to a search engine that contradicted the agreement on the page. That is a manual action risk, and more importantly it is just wrong: machine-readable claims about who you are should be the same claims as the human-readable ones, or there is no point emitting them.
So the rebrand was simultaneously a legal edit, an SEO edit and a data edit. Keeping them in one file is what made it a single edit rather than three that could disagree.
The two lines that re-prompt every account
export const TERMS_VERSION = '1.3';
export const PRIVACY_VERSION = '1.3';
They were 1.2. Who you are contracting with, and who the data controller is, are material changes, which means consent recorded against the old document is consent to a different document.
The mechanism is deliberately small. Account creation records the versions accepted. A tiny endpoint compares what an account recorded with these constants, and when the constants are ahead, a notice appears on the next visit. There is also a script that can email everybody still on the previous version. Bumping the constant is the entire trigger; there is no admin screen, no campaign, no flag.
The notice itself is passive on purpose:
/**
* Passive "we've updated our terms" notice. When a signed-in account's recorded
* Terms/Privacy version is behind the current one, we show this once and record
* their acceptance automatically. There is no "I agree" button; continuing to
* use Munchable is the acceptance. The dismiss only hides it for the session.
*/
No modal, no blocked app, no button to hunt for. The notice says the terms changed and links both documents, and continued use is the acceptance, which is what the terms themselves say happens. An interstitial demanding a click before you can check whether a biscuit is safe to eat would be worse for the user and would not give us better consent than the record we already write.
The part that cannot be automated
What a version bump cannot do is tell you which sentences became false. The deletions in that commit outnumber the insertions because several paragraphs had been written around the previous arrangement: an "about the maker" section, a sentence in the email footer, a comment in the SEO module explaining why no company was named there. Each one was accurate when written and silently wrong afterwards.
There is no type for "this paragraph assumed a fact that has changed". The closest thing I have is the habit of writing the reason next to the decision, so when the reason stops holding, the paragraph that depends on it is the next thing you read. That is why the SEO module's comment explains the relationship rather than just asserting it, and why this one was quick to spot and fix.
Related
Four of our apps share no audience, and one hand-copied TypeScript file is what tells Google they share a publisher is how that file came to exist, and a date is prose, a version is a key is about the versioning scheme the bump above uses. The app these documents govern is at munchable.app.
Top comments (1)
the passive notice is the detail worth stealing. most re-consent flows block the app until you click, which treats consent as a tollbooth. this one treats it as a record, the versions are tracked, the notice informs, nothing holds your session hostage.