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.
4.3 KiB
Nostr layer
Why Nostr at all? Two reasons:
- 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.
- 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_eventbuild_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_pubkeyis 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 30402with#t=menu,limit 200for 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 whensettings.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:
- 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.
- The restaurant's
nostr_syncreceives the wrap, decrypts layers, and produces aCreateOrder. - Order placement goes through the same
services.place_orderpath as REST — including invoice creation. The bolt11 is sent back to the customer pubkey via another NIP-17 DM. - 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
- architecture — extension lifecycle starts the NostrClient
- menu-tree — ancestor names come from here
- order-flow — what NIP-17 will eventually deliver
- webapp-integration — clients of this layer