Skip to main content
RapidDev - Software Development Agency
App Featuresvertical-tools20 min read

How to Add a Digital Menu Builder to Your App (Copy-Paste Prompts Included)

A digital menu builder needs a customer-facing display (category tabs, item cards with photos and dietary icons), a staff CMS for price and availability editing, and Supabase Storage for images. With Lovable or V0 you can ship a working menu in 4–8 hours for $0–25/month at small scale. The tricky parts are JSONB dietary filter logic and image bucket permissions — both of which AI tools get wrong on the first try.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

vertical-tools

Build with AI

4-8 hours with Lovable or V0

Custom build

1-2 weeks custom dev

Running cost

$0-25/mo up to 1K users

Works on

WebMobile

Everything it takes to ship a Digital Menu Builder — parts, prompts, and real costs.

TL;DR

A digital menu builder needs a customer-facing display (category tabs, item cards with photos and dietary icons), a staff CMS for price and availability editing, and Supabase Storage for images. With Lovable or V0 you can ship a working menu in 4–8 hours for $0–25/month at small scale. The tricky parts are JSONB dietary filter logic and image bucket permissions — both of which AI tools get wrong on the first try.

What a Digital Menu Builder Actually Is

A digital menu builder turns your restaurant or café's item list into a live, filterable, photo-rich experience that customers access from any phone — no app download required. Staff update prices and availability in a protected admin panel; changes appear immediately on the customer-facing view. The core product decisions are where images are stored and served (Supabase Storage is the default choice when using Lovable), how dietary filtering works (AND logic, not OR), and whether the menu is view-only or part of an ordering flow. Most early builds are view-only with a QR code link printed on each table — online ordering is a separate feature that requires payment gateway integration.

What users consider table stakes in 2026

  • Category tabs or accordion sections to navigate menu sections without scrolling the full list
  • Item cards with a photo, name, description, price, and dietary/allergen icons (vegan, gluten-free, spicy) visible at a glance
  • Search bar to find items by name or ingredient without tapping through categories
  • Filter panel for dietary preferences and allergens that applies AND logic — vegan AND gluten-free returns only items matching both
  • Real-time price and availability editing for staff without touching code or redeploying the app
  • Mobile-first layout with large tap targets and legible font sizes on a 375px screen

Anatomy of the Feature

Seven components. AI tools handle the card UI and CRUD well on the first prompt. The dietary filter query operator and image bucket policy are the two places builds break silently.

Layers:UIDataBackendService

Category navigation

UI

Horizontal tab bar or vertical sidebar letting customers jump to a menu section without scrolling. Built with shadcn/ui Tabs (web) or React Navigation BottomTabNavigator / Flutter TabBar (mobile). Each tab is backed by a menu_categories row and filters the item grid dynamically.

Note: Accordion sections work better on mobile when there are more than 8 categories — tabs overflow and become unreadable on small screens.

Menu item card grid

UI

Responsive CSS Grid (web) or Flutter GridView.builder (mobile) rendering one card per menu_items row. Each card shows the photo from a Supabase Storage signed URL, name, description, price formatted with Intl.NumberFormat for locale-aware currency, and dietary tag icons. An 'unavailable' overlay (semi-transparent grey) renders when is_available = false.

Note: Use sort_order on both tables to give staff control over display order without redeploying.

Admin CMS panel

Backend

A protected route (/admin/menu) where authenticated staff can create, edit, and delete categories and items. Built with react-hook-form + zod for form validation. Supabase auth role check (e.g., checking a staff_role claim in the JWT) gates access. Staff can toggle is_available and update price without touching code.

Note: Keep the admin route separate from the customer-facing pages — a single shared layout will complicate RLS policies.

Image upload layer

Service

Supabase Storage public bucket receives photo uploads from the admin panel. Images are referenced by image_url stored on menu_items. For on-the-fly resizing use imgix (from $10/mo approx) or Supabase Storage transformation URLs to serve thumbnails at the correct resolution instead of downloading full-size images for card grids.

Note: The bucket must be set to public with an anon SELECT policy — Lovable AI defaults to private, which causes 403 errors in production.

Dietary filter engine

Data

A JSONB column dietary_tags on menu_items stores an array of tag strings (e.g., ["vegan", "gluten-free"]). The filter query uses the PostgreSQL @> containment operator so that selecting multiple tags returns only items tagged with ALL of them — AND logic. Supabase full-text search via to_tsvector on name + description powers the item search bar.

Note: The most common AI mistake is generating an && (overlap) query which gives OR behavior. Always verify the generated query uses @> not &&.

QR code link generator

Service

qrcode.react (web) or qr_flutter (Flutter) renders a scannable QR code pointing to the published customer menu URL. Displayed on a printable /qr page so restaurant owners can export it for table tents. The URL is hardcoded from an environment variable — never generated from window.location.href in a preview environment.

Note: Client-side generation means no API calls and no cost. The library runs entirely in the browser.

Search index

Backend

Supabase full-text search using to_tsvector('english', name || ' ' || description) for menus with hundreds of items. For smaller menus (under 200 items) a client-side ILIKE '%query%' query on the name column is simpler and equally fast.

Note: Add a GIN index on the tsvector column for menus with 500+ items to keep search sub-100ms.

The data model

Two tables cover the full menu structure. Run this in the Supabase SQL editor — it creates the tables, RLS policies, and indexes in one pass.

schema.sql
1create table public.menu_categories (
2 id uuid primary key default gen_random_uuid(),
3 name text not null,
4 sort_order int not null default 0,
5 is_active bool not null default true,
6 created_at timestamptz not null default now()
7);
8
9create table public.menu_items (
10 id uuid primary key default gen_random_uuid(),
11 category_id uuid references public.menu_categories(id) on delete cascade not null,
12 name text not null,
13 description text,
14 price decimal(10,2) not null,
15 image_url text,
16 dietary_tags jsonb not null default '[]',
17 is_available bool not null default true,
18 sort_order int not null default 0,
19 created_at timestamptz not null default now()
20);
21
22-- Enable RLS
23alter table public.menu_categories enable row level security;
24alter table public.menu_items enable row level security;
25
26-- Public can read active categories and available items
27create policy "Public can view active categories"
28 on public.menu_categories for select
29 using (is_active = true);
30
31create policy "Public can view available items"
32 on public.menu_items for select
33 using (is_available = true);
34
35-- Staff (authenticated users) can manage all records
36create policy "Staff can manage categories"
37 on public.menu_categories for all
38 using (auth.role() = 'authenticated')
39 with check (auth.role() = 'authenticated');
40
41create policy "Staff can manage items"
42 on public.menu_items for all
43 using (auth.role() = 'authenticated')
44 with check (auth.role() = 'authenticated');
45
46-- Indexes for performance
47create index menu_items_category_idx on public.menu_items (category_id, sort_order);
48create index menu_items_dietary_tags_idx on public.menu_items using gin (dietary_tags);
49create index menu_categories_sort_idx on public.menu_categories (sort_order);

Heads up: The GIN index on dietary_tags makes the @> containment filter fast even with 1,000+ menu items. The staff-only write policies use auth.role() = 'authenticated' — if you need finer-grained role separation (e.g., servers mark unavailable but only managers change prices), add a profiles table with a role column and extend the policies.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

Full control over POS integration, multi-location menus, role-based pricing, and PDF export — necessary when the digital menu is part of a larger restaurant operating system rather than a standalone display.

Step by step

  1. 1Next.js App Router with Supabase — ISR for the customer menu, Server Actions for admin mutations
  2. 2Supabase Edge Function as a webhook receiver from POS systems (Square, Toast, Clover) to sync item availability and prices in real time
  3. 3Per-location menu variants using a location_id foreign key on both tables, with Supabase RLS filtering by the staff member's assigned location
  4. 4PDF export of the full menu using a Puppeteer-based Edge Function that renders the /menu page with brand fonts and logo to a downloadable PDF

Where this path bites

  • POS webhook integration typically adds 3–5 days to the build timeline and requires a paid POS developer account for testing

Third-party services you'll need

Most menu builder costs are Supabase-driven. Image CDN is optional but significantly improves load time on mobile connections.

ServiceWhat it doesFree tierPaid from
SupabaseDatabase (menu_categories, menu_items), auth for staff login, Storage for item photos500MB DB, 1GB storage, 2 projects$25/mo (Pro — 8GB DB, 100GB storage)
imgixOn-the-fly image resizing and CDN — serves thumbnails at the correct resolution for card grids instead of full-size uploadsNoneFrom $10/mo (approx)
qrcode.react (npm)Client-side QR code generation for the table tent / printed menu linkFree, no API call neededFree
Cloudflare ImagesAlternative image optimization CDN — stores and transforms item photos at the edgeNone$5/mo per 100,000 images (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier covers storage and DB easily. QR code generation is client-side with no API cost. Image bandwidth at this scale stays within free limits.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Images upload in preview but return 403 in production

Symptom: Supabase Storage bucket policies default to private. Lovable AI creates the bucket but does not always add a public SELECT policy for the anon role. In the preview, images work because the Lovable preview session is authenticated as the project owner. In production, anonymous customers get a 403 on every image URL.

Fix: In Supabase Dashboard, go to Storage, select your menu-photos bucket, open the Policies tab, and add a new policy: 'Allow public read' with SELECT operation for the anon role with no conditions. This single policy change fixes all image 403 errors instantly.

Dietary filter returns items that don't match all selected tags

Symptom: AI tools almost always generate the Supabase filter using the && (overlap) operator instead of @> (containment). The overlap operator returns items tagged with ANY of the selected tags — so filtering for 'vegan AND gluten-free' shows every vegan item and every gluten-free item, not just the intersection.

Fix: In the generated Supabase query, change .overlaps('dietary_tags', tags) or .contains (with wrong semantics) to .contains('dietary_tags', tags) which maps to the @> operator in PostgreSQL. Double-check by running the raw SQL: SELECT * FROM menu_items WHERE dietary_tags @> '["vegan","gluten-free"]'::jsonb in the Supabase SQL editor.

Staff menu edits don't appear for customers until page refresh

Symptom: AI-generated menu UIs fetch data once on component mount using a standard useEffect. When a staff member marks an item unavailable or changes a price, the change is written to Supabase but customers see the stale data until they manually refresh the page. This creates a confusing gap especially for availability changes during a busy service.

Fix: Add a Supabase Realtime channel subscription on the menu_items table. In the customer menu component, set up supabase.channel('menu').on('postgres_changes', { event: '*', schema: 'public', table: 'menu_items' }, handleUpdate).subscribe(). This pushes changes to all connected customers within about one second of a staff edit.

QR code in Lovable preview points to the preview iframe URL

Symptom: When qrcode.react generates the QR using window.location.href, it captures the Lovable preview domain (e.g., preview.lovable.app) instead of the published menu URL. Anyone who scans the printed QR code ends up at the preview URL, which may be unreachable or session-gated.

Fix: Store the production menu URL in Lovable Secrets as VITE_MENU_URL and read it in the QR generator component using import.meta.env.VITE_MENU_URL. Never derive the URL from window.location.href at build time.

V0 generates localStorage filter access during SSR, causing hydration mismatch

Symptom: V0 often generates filter state persistence using localStorage.getItem inside a component body. Next.js runs components server-side first, where localStorage is undefined. The server renders one state and the client hydrates with a different state, triggering a hydration mismatch warning and sometimes a visible layout flash.

Fix: Wrap all localStorage access inside a useEffect(() => { ... }, []) hook so it only runs on the client. For the unit preference or filter state, initialize the React state to a safe default (e.g., empty array for filters) and update it in useEffect after the component mounts.

Best practices

1

Store dietary tags as a normalized JSONB array and always query with @> containment — never build OR filter logic for dietary preferences

2

Use sort_order columns on both menu_categories and menu_items so restaurant owners control display order from the admin panel without touching code

3

Serve item photos through an image CDN or Supabase Storage transformation URLs — never load full-resolution uploads in the card grid on mobile connections

4

Make the 'item unavailable' state visually obvious with a greyed-out overlay and 'Currently Unavailable' text, not just a missing price

5

Generate QR codes from a hardcoded environment variable URL, never from window.location.href, so printed table tents survive domain changes

6

Implement Supabase Realtime on menu_items so availability changes propagate to all customers within seconds during service

7

Keep the admin panel on a separate route with an explicit auth check — never rely on hiding the admin link as a security measure

8

Cache Supabase Storage image URLs rather than generating signed URLs on every render — signed URLs are only needed for private buckets

When You Need Custom Development

Lovable and V0 handle single-location, view-only digital menus well. These requirements push past what a two-session AI build can reliably deliver:

  • Menu must sync in real time with a POS system (Square, Toast, Clover) via their webhooks — price and availability changes in the POS should reflect on the menu within seconds without manual edits
  • Multi-location support where each branch has separate pricing, item availability, and portion sizes managed by branch managers with scoped admin access
  • Custom PDF export of the full menu with brand fonts, logo, and layout matching printed menus — required by some franchise agreements and marketing teams
  • Role-based editing where servers can mark items as temporarily unavailable during service but only managers can change prices or add new items

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Can I update prices without redeploying the app?

Yes — that is the core purpose of the admin CMS. Prices and availability are stored in Supabase rows, not in code. When a staff member edits a price in the admin panel and saves, the change is written to the database and immediately visible to customers on the next page load. If you add Supabase Realtime, the change appears on all open customer screens within about one second.

How do I show when an item is sold out or temporarily unavailable?

Set is_available = false on the menu_items row. The customer-facing card renders a greyed-out overlay with 'Currently Unavailable' text. The item still appears in the menu so customers know it exists; it just can't be ordered. Staff flip the toggle back to true when the item is back — no redeploy needed. With Supabase Realtime enabled, the overlay appears within seconds of the staff edit.

What's the best way to handle multiple menu sections (breakfast, lunch, dinner)?

Use the menu_categories table with a sort_order column. Each time period gets its own category row. You can extend the schema with a time_available_from and time_available_to field on menu_categories and conditionally show categories based on the current time in the customer-facing UI. For the simplest build, just use the is_active toggle on categories — staff activate breakfast at opening and deactivate it at 11am.

Can customers order directly from the digital menu or is it view-only?

The menu builder described here is view-only — it shows what's available and at what price, but does not take orders or process payments. To add ordering, you need a cart state, an orders table in Supabase, and payment gateway integration (Stripe is the standard choice). That is a separate build that typically takes an additional 8–16 hours with Lovable or V0.

How do I add allergen warnings that comply with UK or EU food labeling laws?

Add an allergens JSONB array column to menu_items alongside dietary_tags. Populate it with the 14 major EU allergens (celery, cereals containing gluten, crustaceans, eggs, fish, lupin, milk, molluscs, mustard, nuts, peanuts, sesame, soya, sulphur dioxide). Display allergen icons prominently on each card and on the item detail page. For full Natasha's Law compliance in the UK, you also need the ingredient list to be complete and accurate — which requires a content management process, not just a schema.

Can I embed the digital menu on my existing restaurant website?

Yes. The customer-facing menu page is a standard URL that you can iframe into any existing website. The simplest approach is a <iframe src='https://yourmenu.app/menu' style='width:100%;height:800px;border:none'></iframe> tag. If you want deeper integration (shared branding, same domain), deploy the Next.js app on a subdomain and point your existing site's menu link there instead.

What image size works best for menu item photos?

Upload at 1200x800px and let the CDN serve optimized sizes per device. In the card grid, request a 400x300px thumbnail; on the item detail page, load the full 1200x800px image. Supabase Storage transformation URLs support this with a width= query parameter. Keeping originals at 1200x800px balances quality and upload size — photos above 2MB slow the admin panel upload noticeably.

How do I handle seasonal items that appear only at certain times?

The simplest approach: use the is_available toggle on individual items to activate seasonal specials manually at the start of the season. For automated scheduling, add available_from and available_until date columns to menu_items and filter them in the customer query. A Supabase Edge Function triggered by pg_cron can auto-toggle is_available based on the date — this is a 30-minute addition once the base menu builder is running.

RapidDev

Need this feature production-ready?

RapidDev builds a digital menu builder into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.