docs: Obsidian-style vault under docs/

Add a navigable Obsidian vault as the project's first-class
technical documentation. Notes cross-reference with [[wikilinks]];
docs/index.md is the Map of Content.

New notes:
  index.md             MOC, entry point
  architecture.md      what the extension owns vs what lives outside
  data-model.md        entity-by-entity schema reference
  menu-tree.md         the arbitrary-depth tree concept
  order-flow.md        state machine + invoice listener + print
  nostr-layer.md       kinds 0/30402/5/1059, signing, t-tags
  api-reference.md     endpoint catalog by audience
  cms.md               Vue 3 + Quasar 2 UMD conventions, q-tree
  webapp-integration.md  multi-restaurant cart pattern + atomicity
  glossary.md          domain terms

Existing notes (kept as-is):
  adr-0001-menu-tree.md  the storage choice rationale
  design-conversation.md trimmed transcript

README.md adds a Documentation section pointing at docs/index.md
with the headline note list. Each note links to ~3-5 others; the
vault forms a connected graph.

A project-level memory rule (saved outside the repo) commits us to
keeping these docs in sync as the code evolves: any commit that
materially changes schema, API, order flow, Nostr surface, CMS
conventions, or webapp integration must update the relevant note(s)
in the same commit.
This commit is contained in:
Padreug 2026-05-02 09:34:07 +02:00
commit 42a8b08a5b
11 changed files with 1015 additions and 0 deletions

127
docs/webapp-integration.md Normal file
View file

@ -0,0 +1,127 @@
# 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:
```
{
"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):
```js
// 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
- [[architecture]]
- [[order-flow]]
- [[api-reference]]
- [[nostr-layer]]