-
The Waiter offline demo: sample response, Claude not called.
-
Interactive dining journal with illustrative restaurant menus and prices.
-
Saved dinner-plan receipt with simulated group approvals; not a reservation.
-
Set each guest’s tastes, budget and dietary preferences in the local demo.
-
Draggable page-curl interaction in the dining journal.
-
Explore real mapped places; map results are not Qloo-ranked recommendations.
Inspiration
Choosing where to eat with friends is one of the most avoidable arguments of the week. One person is vegetarian, one has a tight budget, someone "doesn't mind" but rejects every suggestion, and the group chat fills up with fifty links and no decision. Most food apps are built for one person scrolling a list. I wanted something that feels like sitting down at a table together: you look at a neighborhood, flip through a menu, set a place for each friend, and when everyone agrees, a small press prints the plan.
I also wanted the app to feel physical. People already know how maps, menu books, place cards and paper receipts work. My rule was that each of those objects had to do real work in the app, not just decorate a dashboard.
What it does
Common Table is an immersive dinner-planning app for groups.
- A real map. The app opens on a real OpenStreetMap view of Brooklyn: OpenFreeMap vector tiles rendered with MapLibre, including real building heights. You can search for any city or turn on device location. Named places in the loaded map become plate, cup and leaf tokens. Each one opens paper "field notes," and you can filter them by Food, Coffee or Outdoors and keep favorites for the session.
- A supper atlas and menu book. A hand-made paper atlas of a fictional neighborhood leads into a menu folio. Drag a page corner and the paper bends, shows its reverse side and reveals the next restaurant underneath.
- A shared table. You can add up to five guests around a rendered dining table. Each place card is a live control for that person's tastes, moods, budget and dietary needs.
- Fair ranking. Every budget and every dietary restriction is a hard rule. Shared taste only decides the order of restaurants that already work for everyone. You can bookmark, veto and undo, and a vetoed venue never quietly comes back.
- Agreement and the press. Pick a venue and a time, then get each guest's approval. Pulling the lever on an olive-and-brass letterpress prints the plan as a receipt, which you tear off at the perforation. Changing anything clears the approvals, so a printed plan is never out of date.
- The Waiter. A brass service bell with a ring-bound pad that knows what you're looking at. It can explain why a place fits, suggest alternatives and walk you through finishing tonight's plan.
The app is honest about its data. The six demo restaurants and their menus are fictional and labelled as such. They never appear on the real map as nearby venues, and real map places never get invented menus, prices or dietary claims.
How I built it
Stack: React 19, TypeScript and Vite on the frontend; a small Node 24 server for the APIs; MapLibre GL with a Leaflet fallback for maps; Blender for the table renders; Playwright and axe-core for testing.
Fair group ranking. Take a table of people $P$, where person $i$ has budget $b_i$ and dietary needs $D_i$. A venue $v$ with price $p_v$ and listed dietary options $\mathrm{diet}(v)$ is eligible only if
$$ v \notin \mathrm{Vetoes} \;\wedge\; p_v \le \min_{i \in P} b_i \;\wedge\; \bigcup_{i \in P} D_i \subseteq \mathrm{diet}(v). $$
Eligible venues are then ranked by
$$ s(v) = \mathrm{clamp}_{[0,\,99]}\Big(\mathrm{round}\big(70 + 19\,\tau_v + \min!\big(5,\ \tfrac{B - p_v}{8}\big) + 10\,(r_v - 4.5)\big)\Big), $$
where $B = \min_i b_i$ is the lowest budget at the table, $r_v$ is the venue's rating, and $\tau_v \in [0,1]$ is the share of the table's taste tags that match the venue's cuisine and tags. Ties are broken by ID, so the order is always the same. The key choice is that constraints filter instead of subtracting points. With a weighted sum, a strong taste match could outvote someone's budget or diet. The ranking sits behind a RecommendationProvider interface so a real recommendation engine can replace it later.
Paper that bends. The menu uses StPageFlip, an MIT-licensed soft-page engine. Out of the box it runs a requestAnimationFrame loop forever, so I patched it to render only while a page is being dragged or settling, and to remove its listeners when the book closes. A turn only counts if the drag goes past both 36px and 28% of the page width. Shorter drags spring back.
A rendered table with live controls. I replaced the CSS illustration with a Python script that builds the scene in Blender (lathed porcelain, linen, glass, wood and chairs) and renders six Cycles images, one for each guest count. The script projects each place card's 3D corners through the camera and saves them to a JSON file. In the browser I compute the homography $H$ that maps a flat card onto that four-cornered shape:
$$ \begin{pmatrix} x' \ y' \ w \end{pmatrix} = H \begin{pmatrix} u \ v \ 1 \end{pmatrix}, \qquad (x,\, y) = \left(\frac{x'}{w},\ \frac{y'}{w}\right), $$
Applying $H$ as a CSS matrix3d puts a real, focusable, screen-reader-friendly name card exactly on the rendered paper. A geometry checker makes sure cards, plates and glasses don't overlap in any of the six layouts before anything is rendered.
The press and the tear. The press is a state machine tied to the plan. It only prints when the schedule is valid and every guest has approved, and any edit cancels the printed paper. The tear follows a ragged seam that advances as you pull. Short pulls spring back, and tap, Enter or Space do the same thing for keyboard users.
The server. Place search goes through a same-origin endpoint that validates input, limits result size, caches answers for 15 minutes, merges duplicate requests and throttles calls to the upstream service. The Waiter has a server-only Claude adapter pinned to Claude Sonnet 5.5. A spend ledger stored in SQLite reserves 1 USD out of a fixed 5 USD allowance before every attempt, so no more than
$$ \left\lfloor \frac{5}{1} \right\rfloor = 5 $$
attempts can ever run, even if every one fails, across restarts and parallel processes. Each request is capped at 16 KiB of text and 512 output tokens. At 10 USD per million output tokens, a reply's output costs at most about half a cent. The browser sends only a short allowlisted context (venue ID, seat count, lowest budget, dietary needs), never guest names, coordinates or notes. For now the browser path returns clearly labelled offline demo answers. Paid browser access stays blocked until proper session authentication exists.
Testing. The project has 87 unit/API tests and an 86-case Playwright browser suite. The browser tests use real pointer drags, Back navigation, mobile layouts and motion fallbacks, and include automated WCAG A/AA scans. Reduced-motion and low-power modes give the same results without the animation. I built it alongside AI coding agents and had a separate reviewer judge real screenshots and recordings, not the code. That loop caught many of the issues below.
Challenges we ran into
- Making paper look like paper. The first menu just rotated a stiff rectangle over the page, and on mobile the rotation stopped before reaching the spine. An opacity animation also flattened the 3D page faces. Switching to a real soft-page engine fixed the look, but taming its endless render loop introduced a subtle bug: a queued page turn could be dropped in the gap between frames. I wrote a regression test that pauses in exactly that gap.
- Making a table look real. CSS shapes looked flat. In the first Blender render, the tablecloth cut through the table and left brown holes in the runner, and the wood grain was too regular. Six seating layouts meant six chances for plates and cards to collide, which is why I wrote the geometry checker. Projected names are tiny on phones, so the guest selector stays as the readable option.
- A tear that actually progresses. The first version released the whole edge at once, and the sawtooth border was just a static image. I rebuilt it as a seam that advances with your pull and leaves a stub in the machine.
- Staying honest inside a convincing world. The better the scene looks, the easier it is to imply data you don't have. I kept a strict line between real map places and fictional demo restaurants. I also built a weather integration and then removed it, because the map should be about real geography, not atmosphere.
- Spending money safely. The budget guard had to hold up under concurrency. The tests have nine processes race for five reservations. They also caught a server bug where a request that failed early left its connection open, which I fixed by closing incomplete connections.
- Layout and accessibility edge cases. Decorative lighting layers made a 1440px-wide window scroll out to 1505px. Accessibility scans were also measuring color contrast in the middle of entrance animations, so they now wait for the animations to finish.
Accomplishments that we're proud of
- Every physical object does real work. The map selects places, the book picks the restaurant, the place cards edit real preferences, and the press saves the real, approved plan.
- Fairness is part of the math. Nobody's budget or diet gets outvoted.
- The full suite passes with zero failures, skips or flaky tests, including the automated WCAG A/AA scans.
- Every gesture has a button or keyboard equivalent, and reduced-motion and low-power modes keep the whole experience working.
- The initial JavaScript bundle is about 139 KB gzipped. The heavy map renderer loads only when needed, and the table images load one arrangement ahead.
- The app labels clearly what is real (map geography and place names) and what is a demo (restaurants, menus, guest approvals).
What we learned
- Hard constraints first, soft preferences second. Filtering before ranking is what makes a group decision feel fair.
- A physical metaphor only works if it's functional. If an object looks nice but doesn't do anything, it just slows people down.
- A still screenshot proves nothing about motion. To verify the page curl and the tear, I had to capture them while a pointer was holding them mid-gesture.
- Pre-rendered scenes plus projection can beat real-time 3D on the web. They load quickly, look rich and keep the controls fully accessible.
- Limits on paid APIs belong in code. They should be persistent, enforced on the server and fail closed, not live in a settings page you hope someone checks.
What's next for Common Table
- Real recommendations. Connect a taste-graph recommendation engine such as Qloo through the existing
RecommendationProviderinterface, with credentials kept on the server and every budget, restriction and veto still enforced. - Verified restaurant data: real menus, prices, opening hours and allergen information.
- Real invitations. Each guest joins from their own phone, sets their own preferences and votes, with live syncing and proper permissions.
- Shareable plans with revocable links and clear privacy rules.
- A live Waiter. Turn on the Claude path once session authentication is in place, with explicit tool permissions: it can suggest, but never book, invite or share a location without consent.
- Booking, so the printed receipt can turn into an actual reservation.
Built With
- axe-core
- blender
- css3
- cycles
- html5
- leaflet.js
- lucide
- maplibre
- node.js
- openfreemap
- openstreetmap
- photon
- pillow
- playwright
- python
- react
- sqlite
- stpageflip
- svg
- typescript
- view-transitions-api
- vite
- web-animations-api
- web-audio-api
Log in or sign up for Devpost to join the conversation.