How an update reaches each place
The planner picked a move — push or hold. Now the publisher Lambda has to turn that into a real change on a real channel: the right format for that place, an owner’s tap when one is needed, the right call to the channel’s adapter, and a record of whether it worked. Get any of those wrong and the update is worse than no update: a price in dollars where the app wanted cents, a description too long for the website, the same change pushed twice. Four small guardrails sit between the move and the change landing.
Key takeaways
- Channel resolution: a per-item channel list beats the per-category default beats the all-channels fallback.
- Routine pushes flow on their own; held changes wait for the owner’s one-tap approval first.
- Each channel has an adapter that formats the change and speaks that channel’s own language.
- Every push carries an idempotency key, so a retry never applies the same change twice.
- The result — accepted or rejected, with the reason — is written to DynamoDB for the next run to read.
Four guardrails on every push
Gate 1: resolve the channels
Three places the publisher looks to decide which channels an item belongs on, in order. First, the item’s own channels column in the master menu — if a dish is set to publish only to the website and the PDF (a dine-in-only special that shouldn’t appear on delivery apps), that list wins. Second, the per-category default in the rules doc (“all drinks publish everywhere except the delivery apps”). Third, the all-channels fallback — if nothing is set, the item goes everywhere. The fallback is the safe default for a normal dish.
For each channel in the resolved list, the publisher picks the right adapter. An adapter is a small piece of code that knows how to talk to one channel: the website’s content store, a specific order platform’s menu API, the PDF generator, the QR-code page. Adding a new channel later is writing one new adapter, not changing the rest of the system.
Gate 2: the approval gate
If the planner’s move was push, this gate is a pass-through — the change was already judged routine and inside the limits, so it flows straight on. If the move was hold, the publisher sends the owner a one-tap approval card in Slack: here’s the item, here’s the old value and the new value, here’s which channels it’ll touch, with Approve and Reject buttons.
Nothing ships to any channel until the owner approves. If they approve, all four gates run as if it were a push from the start. If they reject, the path ends cleanly — the master menu keeps the change (it’s still the source of truth), but the channels are left as they were, and the held item shows up in the weekly digest so it isn’t forgotten. This is the human-in-the-loop guardrail: no surprising change to what a guest is charged ever ships without a person seeing it first.
Gate 3: format for the channel
Channels disagree about everything. The website can show a long, lyrical description; an order app caps it at 120 characters. The website wants a price like “$18.50”; one platform’s API wants the integer 1850 in cents. The PDF wants a category header; the QR page wants a flat list. Gate 3 runs the change through that channel’s formatting template from the voice doc: trim the description to the cap, convert the price to the channel’s units, drop any field the channel doesn’t support, and map the category name to the one that channel expects.
The templates are plain text with placeholders, edited in the voice doc without a deploy. They’re the one place that encodes “this is how the short rib reads on the delivery app versus the printed menu,” so the rest of the system can stay channel-agnostic.
Gate 4: send it once, even on a retry
The last gate attaches an idempotency key — a short fingerprint built from the item, the channel, and the exact value being sent — before calling the adapter. (Idempotent just means “safe to do twice”: if the same change is sent again, nothing extra happens.) If a network blip makes the publisher retry, the adapter sees the same key it already processed and treats the second call as a no-op instead of applying the change a second time.
This matters most for price changes. Without the key, a retry during a flaky moment could bump a price twice, or flip a sold-out flag back and forth. With it, the worst a retry can do is confirm what already happened. The adapter calls the channel — the website content store, the platform’s menu API, the PDF regenerate job, the QR page — and reports back accepted or rejected, with the reason on a rejection. The publisher writes that result to ms-state in DynamoDB. The next run reads it and knows the true state of every channel.
Why the guardrails exist
None of these gates are exotic. They’re the kind of small care a careful manager would take if they were updating each app by hand — check which places this dish even belongs on, get a second pair of eyes on the big changes, format it the way each app expects, and don’t fat-finger the same price twice. Putting them in code as four small sequential gates makes them part of the design, not a habit you’re trusting a busy person to keep on a busy night.
Next post: what happens when a channel says no — how a rejected update gets flagged, surfaced, and fixed, and how the system keeps the menu honest about which places are out of step.
All posts