A menu sync on AWS for a few dollars a month
A restaurant’s menu lives in more places than anyone keeps in their head. The website. The two online-order apps that take a cut of every order. The printable PDF guests download. The QR-code page on the table. When the kitchen runs out of the special, or a supplier raises the price of beef, or the owner adds a seasonal dish, every one of those places has to change — and in real life most of them don’t. The website still lists a dish that sold out at lunch. One delivery app still charges last month’s price. This post walks through the design of a small system where the owner edits one master menu and every change flows out to all the places the menu shows up — and any place that rejects a change gets flagged so somebody can fix it.
Key takeaways
- One master menu in a Drive sheet is the single source of truth; everything else is downstream.
- Every change ends in one of four moves on each run: in sync, push update, hold for approval, or flag a rejection.
- Routine price changes can flow automatically within rules the owner sets; big changes wait for a tap.
- Each place the menu shows up has its own adapter; a place that rejects an update is flagged, not forgotten.
- Designed on AWS for about $3/month at typical single-restaurant volume.
The whole system on one page
Before any code, here’s the shape of what we’re designing.
What you set up once (the outside)
- Master menu. A Google Sheet in a Drive folder, one row per item: name, description, category (starters, mains, drinks, specials), price, a sold-out flag, a seasonal flag, and which channels the item publishes to. You fill it in once and edit it whenever something changes. New changes can also enter via two other lanes covered in Part 2 — a quick-edit lane (mark a dish sold-out from your phone in two taps) and a supplier-price lane (forward a supplier price list and the system proposes the new costs for one-tap approval).
- A rules folder. Two short Google Docs in a Drive folder. The rules doc covers which channels each item category publishes to, the auto-sync limits for routine changes (for example: a price change under 10% can flow automatically; anything bigger waits for a tap), and the approval thresholds for risky moves like removing a whole category. The voice doc holds one formatting template per channel — how a dish name, description, and price should read on the website versus the printable PDF versus an order app that caps the description length.
- Channels. The places the menu shows up. Each channel has a small adapter that knows how to talk to it — the website’s content store, each order platform’s menu API, the PDF generator, the QR-code page. Each adapter reports back whether it accepted the update or rejected it (a price field it wouldn’t take, an item name that’s too long, a category the platform doesn’t support).
What runs on every change (the inside)
- The change intake. Three sources feed the master menu. The Drive sheet itself is the canonical store. Changes can also be added via the quick-edit lane (tap a dish sold-out from a small phone page; the change lands in the sheet) and the supplier-price lane (forward a price list to
prices@your-restaurant.com, the system uses Textract to read it and Bedrock Haiku 4.5 to match each line to a menu item and propose the new price, then drops a one-tap approval card in the owner’s Slack before the price is written). - The planner. Runs whenever the master menu changes. Reads the new menu. For each channel an item publishes to, compares what the master menu now says against what that channel currently shows. Picks one of four moves. In sync: the channel already matches — do nothing. Push update: the change is routine and within the auto-sync limits — send it straight to the channel. Hold for approval: the change is big (a price jump over the limit, a removed category) — hold it and ask the owner to approve before it goes out. Flag a rejection: a previous push was refused by the channel — surface it so somebody can fix the underlying problem. The planner itself doesn’t call a model — the move logic is plain comparison.
- The publisher. Takes the chosen move and the channel’s formatting template, formats the change for that channel, and sends it through the channel’s adapter. Website and PDF changes go straight through; order-platform changes go through each platform’s menu API. Every push writes a row in DynamoDB recording whether the channel accepted it, so the next run can tell what’s in sync and what got rejected. A weekly digest summarizes what changed that week and which places are out of step. A monthly summary writes a short note: how many changes flowed automatically, how many needed a tap, and which channel rejects most often.
In plain words
It’s 11am and the kitchen tells you the slow-braised short rib is 86’d for the day — sold out. You open the quick-edit page on your phone and tap the dish sold-out. Within a minute the website grays it out, both order apps mark it unavailable so no guest can order something the kitchen can’t make, and the QR-code page updates. The printable PDF regenerates overnight. Later that week your beef supplier emails a new price list; you forward it to the prices address. The system reads it, matches the short rib’s cost to the line on the sheet, and proposes a $2 price bump. Because $2 is under your 10% auto-sync limit, you tap approve and it flows everywhere. The one order app that rejected the change — its API wanted the price in cents, not dollars — shows up flagged in your Slack so it gets fixed once instead of silently charging the old price for a month.
The cost of running this is about $3 a month at single-restaurant volume. The cost of not running it is the guest who orders a sold-out dish and waits twenty minutes for an apology, or the delivery app quietly selling your food below cost because nobody updated the price.
Design rules that shaped every decision
- One master menu is the only source of truth. Every channel is downstream; none of them is edited directly.
- Four moves, always. In sync, push update, hold for approval, flag a rejection. There is no fifth.
- Routine changes flow automatically within the owner’s rules; risky ones wait for a tap. Nothing big goes out unseen.
- A rejected update is flagged, never swallowed. A place that’s out of step is visible, not silent.
- The menu lives in Drive. Changing a price, marking sold-out, or adding a dish doesn’t need a deploy.
- Every push is logged. Ask “why does this app still show the old price?” next month and you can see exactly what happened.
Why this shape
Most restaurants keep their menu in one of three ways: a single place they treat as real (usually the website) and a mess of copies they update by hand, a stack of separate logins they remember to touch only when a guest complains, or somebody’s memory of which app has which price. The hand-updated copies drift the moment a busy night arrives. The separate logins are exactly the chore that gets skipped first. And memory fails the day the person who held it is off and the kitchen runs out of the special.
The setup above moves the source of truth into a sheet the owner already edits, but adds a small system that watches that sheet and pushes each change out to every place the menu lives. Routine changes — sold-out, a small price tweak — flow on their own within rules the owner set. Big changes wait for a tap so nothing surprising ships. And when a channel refuses a change, the system says so instead of pretending it worked. The sync is invisible on the days nothing changes; it only shows up when something does — and on the days a channel pushes back.
The next four posts walk through each piece in turn: how a menu change gets made, how a change turns into per-channel updates, how an update reaches each place, and how a rejected update gets fixed. One diagram per post. A cost breakdown and a final engineering reference at the end.
All posts