docs: ADR-0001 nostr event model

Records the decision to compose standard NIPs (99 listing, 78 reservation,
52 calendar, 17/59 DMs) and reserve aiolabs kind:22000/22001 only for the
ephemeral availability RPC. Documents rejected alternatives (bespoke 2200x
booking record, RSVP-as-booking, deprecated NIP-15) and flags the 22000/1
allocation to register in workspace CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
This commit is contained in:
Padreug 2026-07-19 00:16:39 +02:00
commit f0c24ebba6

View file

@ -0,0 +1,66 @@
# ADR-0001 — Nostr event model for room rentals
**Status:** accepted (design phase) · **Date:** 2026-07-19
## Context
Chatelet needs a Nostr representation for: a room listing, a private booking
negotiation, a durable reservation record for the guest, public
availability, and a live "is it free?" query. No single NIP defines a
rental-booking protocol. We must decide whether to invent a bespoke set of
kinds or compose existing NIPs.
Constraints from the workspace:
- **Respect protocol semantics over friction reduction** — pick the kind a
primitive is *for*, not whatever listener happens to exist.
- **CLINK band `2100121099` is OFF-LIMITS.** Aiolabs custom band is
`2200022099`; document each allocation.
- **No nsec at rest** — all signing/encryption via `resolve_signer`.
## Decision
Compose standard NIPs; reach into the aiolabs band only for the one case no
standard kind fits.
| Concern | Choice | Why |
|---|---|---|
| Room listing | **NIP-99 `kind:30402`** | Purpose-built for listings; the NIP names *rentals* explicitly. Has `price` with a `frequency` (`night`), `location`, `g` geohash, images. Addressable (`d`=room id) so edits replace. |
| Guest reservation record | **NIP-78 `kind:30078`** | App-specific replaceable data; workspace already endorses 30078 for app state. Addressable (`d`=booking id) so status transitions replace in place. NIP-44 encrypted to the guest. |
| Public availability | **NIP-52 `kind:31923/31924`** | Calendar events are exactly "these dates are occupied". Published without PII, gated by `settings.publish_availability`, so third-party clients can render open dates without querying us. |
| Private request → quote → confirm → check-in | **NIP-17/59 giftwrap (`kind:1059`)** | The private-messaging standard. Relays store them (survives operator downtime), metadata-private, no bespoke transport to maintain. |
| Live availability query + response | **aiolabs `kind:22000` / `kind:22001`** (ephemeral) | A live ping/pong; no standard kind fits, and it *should* be ephemeral (no need to persist a stale "free?" check). Mirrors how CLINK uses the ephemeral range for payment RPC. |
## Rejected alternatives
- **A single bespoke `kind:2200x` for the whole booking (request + record).**
Rejected: the `2000029999` range is *ephemeral* — relays don't store it,
so a booking request would be lost if the operator were briefly offline.
Persistent booking state belongs in stored kinds (giftwrap DMs + `30078`),
not the ephemeral band. Using ephemeral only for the genuinely-transient
availability ping is the semantically honest split.
- **NIP-52 RSVP (`kind:31925`) as the booking request.** Rejected: RSVP means
"attendance intent for a calendar event" — a commercial reservation with
money and a hold is a different relationship. Repurposing it would conflate
attendance with booking (the exact anti-pattern the workspace CLINK/kind
rule warns against).
- **NIP-15 marketplace order flow (`kind:3001730019`).** Rejected: the NIP
is deprecated ("too complicated, use NIP-99") and its stall/product/order
model is a poor fit for date-ranged, single-inventory lodging.
## Kind allocation register (action required before ship)
`22000` (availability query) and `22001` (availability response) are hereby
claimed for Chatelet in the aiolabs `2200022099` band. **Follow-up:**
record this allocation in the workspace `CLAUDE.md` kind-allocation section
so a future extension doesn't reuse the numbers. Ephemeral, so on-wire
collisions would be transient — but still worth registering.
## Consequences
- The heavy lifting stays in standard NIPs → third-party Nostr clients get
listings + availability for free; less bespoke code to maintain.
- Exactly one custom, low-stakes (ephemeral) kind pair to own.
- Booking *authority* stays in LNbits regardless of event choices — the
events are the interop skin over a DB-arbitrated flow (see
[`event-flow.md`](event-flow.md)).