Prerequisite for the customer webapp module (aiolabs/webapp, branch feat/restaurant-bundle): the webapp's /r/:slug route needs to resolve a slug to a Restaurant payload without an admin key. crud.get_restaurant_by_slug already exists (used by the server- rendered CMS routes in views.py); just expose it as a public REST endpoint. Mirrors api_get_restaurant by id and is declared before the bare-id route so the static prefix wins FastAPI's path match. Verified live against seeded 'Big Jay's Bustaurant': GET /restaurant/api/v1/restaurants/by-slug/big-jays-bustaurant -> 200 with the Restaurant payload.
4.4 KiB
Webapp integration
The customer-facing UI lives in ~/dev/webapp (the AIO webapp).
This note describes the contract between the webapp and any number
of restaurants, especially the multi-restaurant cart pattern.
Discovery
A webapp can either talk to one restaurant directly or aggregate many. There's no central directory inside this extension — grouping ("festival", "collective space", "food court") is emergent via NIP-51 list events curated by whoever runs the venue.
For the single-venue case, a webapp that routes on a URL slug
(/r/big-jays-bustaurant) resolves the slug → restaurant via the
public GET /restaurants/by-slug/{slug} endpoint
(api-reference) and proceeds with that one id for menu reads
and order placement. Slug is just a URL nicety — internally
everything continues to key on the restaurant id.
For the aggregator case (multiple restaurants in one cart), the webapp consumes a curated NIP-51 list event:
{
"kind": 30000, // NIP-51 follow set / generic list
"tags": [
["d", "festival-2026"],
["title", "Atitlan Bitcoin Festival 2026"],
["p", "<restaurant1-pubkey>"],
["p", "<restaurant2-pubkey>"],
["p", "<restaurant3-pubkey>"]
],
...
}
The webapp:
- Resolves the list event for the festival / venue.
- Fans out to fetch each restaurant's profile (kind 0) and menu
(kind 30402) over Nostr, or falls back to
GET /restaurant/api/v1/restaurants/{id}/menuover REST. - Subscribes to each restaurant pubkey for live updates.
No central wallet, no central database, no per-restaurant onboarding flow specific to "joining a festival". Restaurants exist; festivals are curated views.
Multi-restaurant cart
A single customer can put items from multiple restaurants into one cart. On checkout, the webapp issues one order per restaurant; each restaurant returns its own bolt11; the customer pays N invoices. There is no payment splitter and no umbrella order on the server side.
The pattern, in pseudocode (this lives in the webapp, not the extension):
// 1. Group cart by restaurant.
const cartByRestaurant = groupBy(cart.lines, line => line.restaurant_id)
// 2. Pre-flight quote per restaurant.
const quotes = await Promise.all(
Object.entries(cartByRestaurant).map(([rid, lines]) =>
fetch(`/restaurant/api/v1/orders/quote`, {
method: 'POST',
body: JSON.stringify(lines.map(l => ({
menu_item_id: l.id,
quantity: l.qty,
selected_modifiers: l.modifiers
})))
}).then(r => r.json()).then(j => ({rid, lines, msat: j.required_msat}))
)
)
// 3. Fail fast on insufficient balance.
const totalMsat = quotes.reduce((s, q) => s + q.msat, 0)
if (walletBalanceMsat < totalMsat) {
return showInsufficientBalanceError()
}
// 4. Open one order per restaurant.
const orders = []
for (const q of quotes) {
const res = await fetch(`/restaurant/api/v1/orders`, {
method: 'POST',
body: JSON.stringify({
restaurant_id: q.rid,
items: q.lines.map(asCreateOrderItem),
customer_pubkey: window.user.nostrPubkey,
parent_order_ref: cart.id,
tip_msat: q.tipMsat,
payment_method: 'lightning'
})
}).then(r => r.json())
orders.push(res)
}
// 5. Pay each bolt11 in sequence.
for (const o of orders) {
await payInvoice(o.invoice.bolt11)
}
Atomicity
Sequential per-restaurant payments are best-effort, not atomic. In practice — for internal LNbits transfers — payment failures are rare and usually transient. If one of N invoices does fail mid-flow:
- The successful orders are already paid; their restaurants will print and prepare them.
- The failed order has issued an invoice that simply expires.
- The customer settles the gap in person at the failing restaurant (cash, retry, or refund of the others).
The pre-flight /orders/quote step plus a balance check before
opening any invoice eliminates the most common cause (insufficient
funds). HODL invoices for true atomicity are on the roadmap and
would replace step 4 once they ship.
parent_order_ref
The webapp may pass an opaque parent_order_ref (e.g. its own
cart id) when posting /orders. The extension stores it on the
orders row but never reads it — useful only for the webapp to
correlate its umbrella cart with the per-restaurant orders later.