Munchable is a gut health scanner: you scan a barcode, it tells you whether that product fits the conditions on your profile, and it names the reason. We make short vertical videos for it. Sixteen of them at the moment, each one a phone, a shopping aisle, a scan, and a result card that says "Good fit", "Caution" or "Avoid".
A video like that is a claim. It is also the only artefact in the whole product that nobody re-renders when the code changes. A screen recording made in March is still sitting on a feed in September, showing a verdict the app stopped giving in June. There is no test that fails, no type error, no alert. The video just quietly becomes a lie.
So the one rule in our reels package is that no composition may contain a hand-typed verdict.
The engine is a dependency of the video
The reels are Remotion compositions: React components rendered to 1080x1920 at 30 fps. That means every reel is an ordinary TypeScript project in the monorepo, and it can import the same package the app imports.
// packages/reels/src/verdicts.ts
import { fitCheck } from '@munchable/rules-engine';
const cache = new Map<string, FitCheck>();
export function assess(def: ProductDef, profile: Profile): FitCheck {
const key = `${def.id}|${JSON.stringify(profile)}`;
let fit = cache.get(key);
if (!fit) {
fit = fitCheck(engineProduct(def), profile);
cache.set(key, fit);
}
return fit;
}
fitCheck is the production function. It is the same call the scan endpoint makes when a real person points a real phone at a real jar. The composition passes it an illustrated product and a profile, and whatever comes back is what the card draws: the word, the colour, the icon, the named trigger underneath.
The cache is there because of how Remotion works. A 25 second reel is 750 renders of the same component tree, one per frame, and each of those renders would otherwise re-run the assessment for a product that has not changed. Memoising by product and profile turns 750 evaluations into one.
The consequence is the point. If a rule changes so that a product in a reel now scores differently, the next render of that reel shows the new verdict. We cannot ship a video that promises something the app would not say, because the video asks the app.
The cast is generic, and the ingredients are real
Every pack in a reel is an invented brand. Real packaging in a paid placement is a trademark problem, so the cast is a set of illustrated cartons, tubs and jars in our own palette, deliberately never green and never red so that nothing on a shelf reads as a verdict before the scan happens.
The ingredient lists on those invented packs are real. That is what makes the demo honest: the engine is given an ordinary ingredient list of the kind you would find on the back of an ordinary pack, and it works out the answer from there.
Which introduces the one place drift could still get in. Each illustrated product carries the ingredient ids typed by hand beside the ingredients text, and nothing in the type system stops those ids from being wrong. A typo there does not crash anything. It scores the product against a tag the engine has never heard of, quietly applies the unknown-ingredient penalty a real product would never get, and freezes the reel's verdict away from whatever the real id later comes to mean.
One test closes it:
test('every reel product is built from ids the engine knows', () => {
const unknown: string[] = [];
for (const [key, product] of Object.entries(PRODUCTS)) {
for (const tag of product.ingredientsTags ?? []) {
if (!isKnownTag(tag)) unknown.push(`${key}: ${tag}`);
}
}
assert.deepEqual(unknown, []);
});
That is the whole test. It is not clever and it does not need to be. It exists so that "the cast the reels score is the cast the app would score" is a thing CI checks rather than a thing a comment claims.
The look came from a comment that promised it mirrored the app
The colours had the same disease. The reels package used to carry its own copy of the app's palette, type scale and verdict vocabulary, under a comment saying it mirrored apps/mobile/src/theme/tokens.ts. A copy maintained by a comment drifts on the first hurried afternoon. Change the caution colour, or reword the "Can't assess" label in the app, and the marketing videos keep rendering the old one with no type error anywhere in the repo.
So the platform-free half of the tokens moved into @munchable/design-tokens: the palette, the spacing and radius scales, the verdict labels and icons. The mobile app re-exports that package and adds what only React Native needs, which is the motion curve, the shadow and the font family names Expo loads. The reels import the same package and add what only a video needs, which is CSS font stacks and pixel line heights. App code still imports everything from one file, and there is now exactly one definition of what "Caution" is called and what colour it is.
Even the small print comes from the engine. The disclaimer under a result in a reel is the same exported string the app renders, so the sentence people read in a video is the sentence they will read on the screen.
Check the engine yourself
You do not have to take any of this on trust, because the same engine answers a few hundred public pages.
- Does E330 cause reflux? is a page whose answer is produced by running the engine at build time, additive number and all.
- The full question index is every one of those pages, and they are generated from the same source of truth as the app's result screen and the reels' result card.
- The conditions we cover is the vocabulary all three surfaces share.
If you go through the index and find a page whose reasoning does not match what the app tells you on a scan, that is a bug in one deterministic function rather than a difference of opinion between a marketing team and an engineering team. That is the trade we wanted.
We wrote earlier about the same idea applied to SEO pages, in Our generated SEO pages run the production engine, and the fixture rotted. Videos turned out to be the harder case, because a page can be rebuilt on the next deploy and a published video cannot be rebuilt at all.
Top comments (0)