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:
parent
7f7915a041
commit
42a8b08a5b
11 changed files with 1015 additions and 0 deletions
110
docs/nostr-layer.md
Normal file
110
docs/nostr-layer.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue