restaurant/docs/nostr-layer.md
Padreug 42a8b08a5b 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.
2026-05-09 07:11:06 +02:00

4.3 KiB

Nostr layer

Why Nostr at all? Two reasons:

  1. Live menu propagation. Customer apps subscribe to a restaurant's pubkey and pick up menu changes (new items, price updates, sold-out states) without polling.
  2. Cross-instance discoverability. A festival or food court curator publishes a webapp-integration of restaurant pubkeys; any client can resolve it into a unified menu without needing to know which LNbits instance hosts each restaurant.

What gets published

Kind Source When
0 (NIP-01 metadata) restaurant profile restaurant create / update
30402 (NIP-99 classified listing, parameterized replaceable, d-tag = item id) menu items item create / update; node rename re-publishes the whole subtree's items
5 (NIP-09 deletion request) menu items item delete

Menu listings carry structured tags so subscribers can filter without parsing markdown:

Tag Format Purpose
d item.id addressable identifier
title item.name listing title
summary first 140 chars of description preview
price ["price", n, currency] structured price (NIP-99)
image url one per image, repeatable
t "menu" universal anchor
t <slug> per ancestor root-first, slugified to lowercase ASCII (e.g. hot-beverages); lets clients filter by category
t dietary tag vegan, gluten_free, etc.
t allergen:<x> structured allergens
t ingr:<x> structured ingredients
l "restaurant:<id>" back-link to the operator
location restaurant location physical reference
g restaurant geohash geo-filterable
status "active" or "sold" NIP-99 sold-out state

Builders live in nostr_publisher.py:

  • build_restaurant_metadata_event
  • build_menu_item_event(..., ancestor_names=...)
  • build_delete_event

_slugify produces the ancestor t tag values. Renaming a menu-tree re-publishes every item in the subtree so the new tag set lands.

Signing

Each restaurant has an effective Nostr identity:

  • If restaurant.nostr_pubkey is set, that's a per-restaurant identity (storage of the matching secret key is out of scope in v1; the column is informational until a vault is wired up).
  • Otherwise, the LNbits Account keypair of the wallet owner is used (account.pubkey / account.prvkey).

nostr_publisher.publish_event(client, event, prvkey) signs in place with coincurve.PrivateKey.sign_schnorr (BIP-340) and ships to the relay via the architecture internal WebSocket.

What gets listened for

nostr_sync.wait_for_nostr_events subscribes to:

  • kind 30402 with #t=menu, limit 200 for backfill, then live. Currently used only as an echo confirmation of our own publishes; federated foreign-menu indexing is on the roadmap.
  • kind 1059 (NIP-17 gift-wrapped DMs), only when settings.nostr_orders_enabled. The unwrap step (NIP-44 v2) is stubbed — the dispatcher (_place_order_from_dm) is complete and ready for the decryption hook.

NIP-17 order intake (planned)

The intended flow once unwrap lands:

  1. Customer's webapp encrypts an order payload with NIP-44 v2 to the restaurant's pubkey, gift-wraps it (kind 13 → kind 1059), and publishes.
  2. The restaurant's nostr_sync receives the wrap, decrypts layers, and produces a CreateOrder.
  3. Order placement goes through the same services.place_order path as REST — including invoice creation. The bolt11 is sent back to the customer pubkey via another NIP-17 DM.
  4. Status updates (paid → preparing → ready) flow back the same way.

REST stays the supported transport until that lands, since LNbits already has tested invoice plumbing.

What does NOT get published

menu-tree themselves. They're internal organizational structure; only items and the restaurant profile carry public Nostr identity. If we ever want categories to be discoverable as standalone entities, NIP-51 lists are the right vehicle, not a new kind.

See also