Shops templates

Full design · Shops

Restaurant

A complete site and booking system for a wood fired neighbourhood bistro.

Live preview. Click around, it works.

About this template

A complete site and booking system for a wood fired neighbourhood bistro. Guests see a photography led home page with tonight's specials, a real HTML menu by course with dietary tags, allergens and a set menu, and book a table from a real slot engine that respects service times, table capacity and existing bookings, with a waitlist when full and a confirmation code they can use to change or cancel. A private dining enquiry goes through a validated form, and gift vouchers have a demo checkout. The host side has a table plan for tonight drawn as a live floor plan, a searchable bookings list with status and notes, a menu editor that can 86 a dish on the spot, and settings for service times, table capacity and the cancellation policy that actually changes what the booking engine allows.

What the agents are asked

The first message sent when you use this template. You can edit the plan before anything is built.

Adapt this wood fired bistro to my restaurant. Keep the guest side (Home, Menu, Reservations, Private dining, Gift vouchers) and the host side (Tonight, Bookings, Menu editor, Settings), and keep the reservation slot engine, the waitlist and the cancellation window working. Change the brand, colours, address, service times, menu and dishes, table layout and team to match my restaurant, and replace the sample photography only when I supply real photographs of my own food and room. Where I want real accounts, real payments for vouchers or emailed confirmations, wire in authentication, the database in schema.sql, Stripe Checkout and an email service as the README describes, never putting a secret key in the browser.

A complete site and reservation system for a wood fired neighbourhood bistro: photography led marketing pages, a real HTML menu with dietary tags and a set menu, a reservation engine that respects service times, table capacity and every existing booking, a waitlist when a service is full, a private dining enquiry form, and demo gift vouchers. The host side runs the floor: tonight's covers and table plan, every booking, the menu (including 86ing a dish on the spot), and settings whose numbers actually change what a guest can do.

This template is original work for NovaBuild. It is not modelled on a specific real restaurant.

Files

FilePurpose
template.jsonslug, name, category, description, prompt, tags, identity
media.jsonthe photographs and one video generated for this template
index.htmlthe entry point; one <script type="module" src="./script.js">
styles.cssdesign tokens first, then components
script.jsone plain ES module: helpers, seed data, store, views, routing, events
schema.sqlthe Postgres tables, row level security and functions the real app needs

Roles and the Viewing as switch

The ribbon at the top switches between Guest and Host, kept in localStorage so a reload remembers it. In a real deployment this becomes: anyone signed out, or signed in without being the restaurant's account, sees the guest side; the account whose restaurant_settings.owner_id matches auth.uid() sees the host side (is_host() in schema.sql, the same idea as a normal login gate). This template does not include sign in itself; see "Auth" below.

Screens

Guest

  • Home: photography led, tonight's specials pulled live from the menu (86 one and it shows as sold out here too), the room, the kitchen, the team, a link into the set menu and private dining.
  • Menu: every dish by course, dietary and allergen tags, filters that actually hide non-matching dishes, a market price row, and the set menu.
  • Reservations: party size, a date and service picker that only offers times the kitchen is open, a slot grid that reflects real table capacity and every existing booking, a waitlist prompt when a whole service is full, guest details, review, and a confirmation with a code (the signature "stamp" moment) and an .ics download.
  • My booking: look up a booking by its code and email, change the time (re-runs the same engine) or cancel inside the policy, with a plain warning when a change is inside the cancellation window.
  • Private dining: a validated enquiry form for the Firebox, written through submit_private_dining_enquiry() in schema.sql, never a direct table insert (see "Security" below).
  • Gift vouchers: choose an amount, a demo checkout, a printable looking voucher card, and a balance check by code.

Host ("Viewing as: Host")

  • Tonight: covers by service, and a floor plan drawn in SVG. Every table's fill colour comes from its current or next booking today: free, booked, arrived, seated or no show. Click a table to see and act on its bookings for the night.
  • Bookings: three tabs. Reservations (search by name, email, phone or code, filter by status and date, open any booking to change status, reassign its table or add a host note). Waitlist (seat a waiting party into the next free slot with one click). Private dining (work an enquiry through new, replied, confirmed or declined).
  • Menu editor: 86 a dish and the guest Menu page stops offering it immediately; feature it as tonight's special and it appears on Home; add, edit or remove dishes.
  • Settings: service times per day (lunch and dinner independently), slot length, notice and booking window, table list (add, edit, retire), and the cancellation policy: window, fee, party limits, waitlist on or off, and the private dining minimums. These are read by the same functions a guest's booking runs through, not just displayed: lower the cancellation window to zero and a booking's late fee warning disappears immediately, on both sides.

Art direction

One shoot, one grade: warm evening service light, tungsten pendant lamps mixed with the glow of the wood fired oven, deep real shadows, a 50mm f/2 look with fine film grain, held constant across the room, the kitchen, the team portrait and every dish. Square corners throughout, generous negative space, tight or unexpected crops on the food (media.json has every prompt). Nine photographs, each one subject, never reused: the room (also the hero's five second video loop), the kitchen, the team, and six signature dishes that each get their own photograph; every other menu item is a typeset row rather than a stand in photo, so no two dishes ever share one.

The reservation engine

Deliberately checks capacity two different ways:

  1. Guest facing availability (available_slots()): a time is offered only when at least one bookable table (window, dining, bar or communal zone; the Firebox is never sold online) fits the party size and is free for the party's whole visit, including the table turn time (90 minutes for 1 to 2 guests, up to 135 for 7 or 8). This is real per table capacity, not a headcount ceiling: a service with plenty of empty two tops can still be "full" for a party of six if every six top is taken.
  2. Booking: create_booking() assigns the smallest table that fits, re-checking availability at the moment of booking, with a Postgres exclusion constraint on bookings as the final word if two guests ever raced for the same table.

The Tonight floor plan is a separate, later decision: a booking's table_id can be moved to any free table any time before the guest arrives, which is what "reassign" in a booking's detail sheet does. This split (capacity for availability, tables for seating) is a simplification worth naming: a real dining room can sometimes seat a party of six across two joined four tops, and this template does not model joining tables. Add that as a second pass in _best_table() if a restaurant needs it.

Data model

Nine tables in schema.sql, matching store in script.js field for field: restaurant_settings (singleton), dining_tables, menu_categories, menu_items, set_menus, bookings, waitlist, private_dining_enquiries, gift_vouchers. The script's data layer is one object, store, with the same shape; swapping localStorage for Supabase is a change inside store only (see "Plugging in a real backend").

Security

Every table has row level security, and anon holds no insert, update or delete grant on any table (tools/rehearse-template-schema.mjs checks this). Every guest write, booking, finding or cancelling a booking, joining the waitlist, sending a private dining enquiry, buying a voucher, goes through a SECURITY DEFINER function that validates its own inputs and never trusts a client supplied status (lesson L25). submit_private_dining_enquiry() is the clearest example: it fixes status to 'new' no matter what is sent, and refuses a second enquiry from the same email inside ten minutes. The host reads and writes bookings, dining_tables, menu_items and the rest directly, gated by is_host().

What is sample or simulated

  • Every sample row carries is_sample: true and a Sample tag wherever it appears. Settings and sampleVersion are described in TEMPLATES.md; SAMPLE_VERSION in script.js governs when the sample catalogue (tables, menu, categories, set menu) and sample activity (bookings, waitlist, enquiries, vouchers) are replaced on a returning visit, keeping anything a real guest did.
  • Gift vouchers are a demo checkout: no card is charged. purchase_gift_voucher() is the seam where a real Stripe Payment Intent belongs; the row it inserts is exactly what a successful webhook would otherwise write.
  • Confirmation emails are not sent. The private dining form and the booking confirmation both say so in the page.
  • Settings > Data removes every sample row in one step (the hard rule that sample data must be removable in one step), or resets everything back to the original sample set.

Plugging in a real backend

1. Database

Apply schema.sql to a Supabase project (or run it through NovaBuild's own migration flow). It is additive and idempotent: re-running it does nothing to existing rows.

2. Swap the store

Every read and write in script.js goes through store (store.all(), store.get(), store.settings(), store.rpc.*, and the host mutation methods). Replace the body of each with a Supabase call, keeping the same function names and return shapes:

// before
availableSlots({ partySize, dateKey, service }) { /* localStorage math */ }

// after
async availableSlots({ partySize, dateKey, service }) {
  const { data, error } = await supabase.rpc('available_slots', { p_party: partySize, p_day: dateKey, p_service: service });
  if (error) throw new AppError('FULL', error.message);
  return data;
}

store.rpc.createBooking, findBooking, cancelBooking, rescheduleBooking, joinWaitlist, submitPrivateDiningEnquiry, purchaseGiftVoucher and lookupVoucher map one to one onto the Postgres functions of the same name (with the Postgres naming: create_booking, and so on).

3. Payments (Stripe)

purchase_gift_voucher() is the whole surface: create a Stripe Checkout session server side for the chosen amount, and have the webhook call purchase_gift_voucher() (or insert the row directly with provider = 'stripe' and the session id as provider_ref) once payment succeeds. Never create the Stripe secret key client side; it belongs in a server function or edge function.

4. Email

Three moments want a real email once a provider (Resend, Postmark, SES) is connected: a booking confirmation and its .ics attachment, a private dining enquiry reaching the restaurant's inbox and a reply reaching the guest, and a gift voucher's code reaching the recipient. Send from the same server function that calls the matching Postgres function, after it returns successfully, never from the browser.

5. Auth and roles

restaurant_settings is a singleton (one row per project) and its owner_id starts null: on a fresh Supabase project nobody, the restaurant's own owner included, can pass is_host() until something sets it, so no manual SQL editor step is the way in any more (LESSONS.md L60). The first person who signs in with Supabase Auth and a confirmed email and calls claim_restaurant() becomes the host: it sets owner_id = auth.uid() under an advisory lock (so two people racing to sign up at once cannot both become the host) and refuses everyone after the first. is_host() already reads owner_id, so every row level security policy in schema.sql starts working the moment it is set, no other change required. Confirm email must be turned on in Supabase Authentication settings before this goes live, or someone could claim the restaurant with an address they do not actually control. This template does not yet wire a real sign in screen into script.js (the "Viewing as" ribbon is a local demo toggle, not Supabase Auth): swap it for one and call claim_restaurant() once signed in, the same way schema.sql's other public functions are already called from a real backend build.

Adapting it

Change the brand, colours, address, service times, menu, table layout and team to match a real restaurant. Photography, the room video and the identity in template.json are this template's own and should be replaced with the customer's real photographs. Two things intentionally not built, called out here rather than left silent: a host side form to take a booking over the phone (schema.sql already has bookings.source = 'host' ready for it, calling the same create_booking()), and joining two tables to seat a party larger than any single table (see "The reservation engine" above).