diff --git a/docs/adr-0001-nostr-event-model.md b/docs/adr-0001-nostr-event-model.md new file mode 100644 index 0000000..763188a --- /dev/null +++ b/docs/adr-0001-nostr-event-model.md @@ -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 `21001–21099` is OFF-LIMITS.** Aiolabs custom band is + `22000–22099`; 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 `20000–29999` 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:30017–30019`).** 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 `22000–22099` 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)).