restaurant/docs/webapp-integration.md
Padreug 6dae57f3f4 feat(api): public GET /restaurants/by-slug/{slug}
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.
2026-05-11 19:17:35 +02:00

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:

  1. Resolves the list event for the festival / venue.
  2. 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}/menu over REST.
  3. 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.

See also