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.
110 lines
4.3 KiB
Markdown
110 lines
4.3 KiB
Markdown
# 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|NIP-51 list]] 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|menu node]] 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|nostrclient extension's]]
|
|
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|Menu nodes]] 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
|