Field Notes · 7 MIN

Building a QR menu SaaS for restaurants

How we built Nactore Menu, a QR menu SaaS for restaurants: one deploy for every restaurant, yearly billing, an operator console, live updates and the check we run after every deploy.

By NactorePublished 25 Oct 2026All articles

A QR menu SaaS has to do four things well: load a menu fast on any phone, give the owner an editor they can use mid-service, bill in a way that never breaks a printed QR code, and give the operator one console to run it all. We built Nactore Menu on that brief. This post covers the architecture, the billing model, the console and what broke.

Key takeaways
  • One frontend deploy can serve every restaurant when the app picks its view from the hostname it loads on.
  • Adding a restaurant is a database row and two Cloudflare API calls, with no new release.
  • Yearly plans are stored as billing periods, and expiry is display-only, so a printed QR code keeps working.
  • Open menus poll every 25 seconds, and the server answers an unchanged menu with an empty 304.
  • Every change ships to a demo restaurant first and reaches real restaurants only after approval.
  • Nactore is a technology and growth company that builds, ships, ranks and grows products like this one, measured on your growth.

What does a QR menu SaaS need to do?

A QR menu SaaS needs to show a restaurant's current menu on a diner's phone the moment they scan, without an app download, and let the owner change dishes, prices and availability in seconds. Behind that, the operator needs onboarding, branding, billing and support in one place, or the business does not scale.

We split the product into four surfaces, all served by one build.

SurfaceWho uses itWhat it does
Product siteRestaurants deciding to buyExplains plans, sends enquiries to WhatsApp
Staff consoleOur operatorsCreates restaurants, branding, plans and payments
Live menuDinersThe menu on the restaurant's own subdomain
Owner consoleRestaurant staffEdits the menu, takes orders, prints bills

There is no self-serve signup. We create each restaurant and its owner login ourselves, which keeps quality high while the product is young.

How does one deploy serve every restaurant?

One deploy serves every restaurant because the app decides what to show from the hostname it is loaded on. Each restaurant gets its own subdomain pointing at the same static build on Cloudflare Pages, so adding a restaurant means a database row plus two Cloudflare API calls, never a new release or a separate site.

Restaurants sit one level below our domain because Cloudflare's Universal SSL covers the root domain and first-level subdomains. A deeper level would need a paid certificate. Pages also caps custom domains per project, so the next step is already planned: a wildcard Worker route before we get near the cap.

A new subdomain is created approved but offline. Only one code path, "Approve and go live", calls Cloudflare, and the public menu answers only for restaurants marked live. This is the same one-backend pattern we describe in one backend, many products.

How should a menu SaaS bill restaurants?

We bill restaurants yearly, and every paid year is stored as its own billing period with a start date, an end date, the amount and its own invoice. A renewal starts the day after the last period ends. Expiry shows in the console but never switches a menu off, so a printed QR code cannot die unannounced.

That last rule is a product decision. A restaurant prints the QR code on tables and walls. If a missed renewal took the menu offline during dinner service, diners would see an error and the owner would lose trust in one evening. So the console shows renewal states (active, due, expired), and we message the owner before a year ends.

We moved from a one-time price to yearly plans after launch. The one restaurant on the old one-time price kept it as a legacy period, with its invoice unchanged. Current plans are listed on the product site.

What goes into the operator console?

The operator console is where we run the whole product: creating a restaurant and its owner login, branding, plan and payment, and handover. Payment fields are visible to staff only and never sent to the owner or the public API. Subdomain and status are read-only, because renaming a subdomain would break every QR code already printed.

Branding is deliberately small. Each restaurant picks a theme, one of four curated font pairings, an accent color, a logo and a hero photo. Images are resized in the browser and stored inline, because the host's disk does not persist and object storage was not worth it for two images per restaurant. The console shows a live phone preview that renders the real menu component, not a mock, so the preview cannot drift from what diners see.

The owner side grew into a back office: an orders board, a tables view, counter orders, settling a bill with a payment method, a printable 80 mm guest bill, and tax settings. Table ordering moves each order through placed, cooking, served and paid, with three taps for the restaurant.

How do open menus stay current without a reload?

Open menus poll the server every 25 seconds while the tab is visible, and the server answers an unchanged menu with an empty 304 response built from a cheap fingerprint query. When an owner marks a dish sold out, an open phone shows the change within about half a minute without a reload, and the sold-out dish disappears.

The fingerprint matters more than the poll. Menu rows with inline images are heavy, so reading them on every poll would burn the database's monthly data allowance. The fingerprint comes from a few aggregate queries, and full payloads are cached by that fingerprint. The static side sits on Cloudflare, the setup we describe in running products on Cloudflare. Order updates can also travel over a realtime broadcast channel, with polling as the fallback.

What we check after every deploy

A build that goes out missing one setting, such as the API base URL, would make every menu call the wrong address. The value is now pinned in the production config, and after every deploy we load a real restaurant's menu and check that it renders.

We also learned that a pushed commit is not a deploy. Frontend changes once sat finished but unreleased for days, because the deploy is a separate manual step. Now "is it live?" is checked on the live site, not in git. Both rules sit in our release checklist, the same one in from prototype to production.

The rollout rule came from the same caution. Every product change goes to a demo restaurant first. Only after approval does it reach real restaurants, and anything a customer would notice without being told gets extra scrutiny.

Pro tip

Never let billing state reach the thing a customer printed. Put expiry in the console and in a message to the owner, not in the public menu.

Frequently asked questions

Does a QR menu need an app?

No. The QR code opens the menu in the phone's browser. Diners download nothing, and the menu updates the moment the owner saves a change.

Can a multi-tenant SaaS run on a static host?

Yes. Our menus are one static build on Cloudflare Pages that reads the hostname to decide which restaurant to show, with a shared API behind it for data.

Why yearly billing instead of monthly?

Yearly periods keep the bookkeeping simple, with one period, one invoice and one renewal date per restaurant. Whatever the cycle, keep expiry away from the live menu.

Can you build a multi-tenant product like this for us?

Yes. This is the kind of work on our work page, from architecture and billing to the operator console.

Build the boring parts first

The menu is the visible part. What makes a menu SaaS work is the boring layer: onboarding without redeploys, billing that cannot break a printed code, and a deploy check that catches a missing setting in minutes. Nactore is a technology and growth company, one partner that builds, ships, ranks and grows, measured on your growth.

Planning a SaaS for a specific industry? Book a free 30-minute call and tell us what your customers print, click or pay for.

Want to apply this to your business?

Book a free 30-minute call. We will tell you what we would do first.