Root cause, reproduced live against checkstr.atobitcoin.io with a real browser: the default relays were bad. wss://relay.damus.io was returning 503 at the WS handshake, and wss://nos.lol accepted the connection but silently never sent an OK for a brand-new pubkey's first event -- no error, just permanent silence. Promise.any over both left the check-in dialog frozen indefinitely with zero feedback. - Swap default relays to three that were actually verified (via a raw WebSocket test, bypassing any client library) to send an explicit OK within a couple seconds for a fresh key: relay.primal.net, offchain.pub, relay.nostr.wirednet.jp. - Add a hard client-side timeout (10s) around every publish, normalizing both failure paths (AggregateError from all-relays-rejected, or nothing responding at all) into one clear message instead of an indefinite hang. - Disable the Check In/Check Out buttons and show a pending label while a publish is in flight, instead of giving no feedback at all.
Checkstr
Hotel-style check-in / check-out, with Nostr as the data layer instead of a database. No backend at all — it's a static page that talks straight to Nostr relays.
How it works
- Identity: sign in with a NIP-07 browser extension (Alby, nos2x, ...),
or generate a throwaway local key ("demo key") for trying it out. The demo
key lives only in
localStorage— don't use it for real guest data. - Room status: each room's current state (vacant/occupied, guest name,
check-in time) is a NIP-33 addressable event, kind
33001, withd = "<propertyId>:<roomId>". Publishing a new one for the same room replaces the old one on any relay that implements addressable events correctly — there's no "update a row," you just publish the new state. - Activity log: every check-in/check-out also publishes a plain kind-1
note tagged
#t checkstrand#t checkstr-<propertyId>, so there's a human-readable history (and it's incidentally visible to any regular Nostr client browsing the#checkstrhashtag). - Rooms list: which room IDs exist for a property is local config
(
localStorage), not a Nostr event — Nostr tracks occupancy state, not the hotel's floor plan.
33001 is not a registered NIP kind, just picked clear of the well-known
reserved ones (long-form 30023, live event 30311, classified listing
30402/3, calendar 31922/3, app handler 31989/90, git repo 30617/8).
Addressable events are scoped per (pubkey, kind, d-tag), so this only
matters if another app renders unrelated JSON under the same kind — worth
registering properly if this grows past a demo.
Running it
It's three static files — no build step:
cd checkstr
python3 -m http.server 8787
# open http://localhost:8787
Or host it anywhere static (GitHub Pages, nginx, etc.).
First run: pick relay(s), a property ID (anything unique to your hotel — it's just a string, no registration), and a comma-separated room list, then connect. Everything after that persists in relay storage + your browser's localStorage.
What's actually been verified
- Event construction (kind 33001 + kind 1), signing, and publish were
tested against a real relay — explicit
OKresponses confirmed for both. - Addressable replace-on-publish (NIP-33) and filter-based subscribe
(
kinds+authors+#t) were confirmed round-trip correct via a raw WebSocket test against a real relay, bypassing any client library. - Not tested: the actual browser UI end-to-end (this environment has no browser with a NIP-07 extension installed). The "demo key" path should let you self-test without one — worth doing before relying on this for anything real.
- One thing worth knowing if you hit it:
nostr-tools'SimplePoolchoked (in a Node test harness, not necessarily in a browser) talking to an oldernostr-rs-relay(0.8.9) — a raw hand-written REQ to the same relay worked fine. If a specific relay seems to silently never respond, it's worth suspecting the relay/library combo before suspecting this app's event shapes. - Public relays vary a lot in how they treat a brand-new pubkey's first write — some (nos.lol, relay.primal.net) let the publish time out silently rather than reject it outright; a NIP-07 identity with some existing history on a relay will likely fare better than a fresh demo key.
Known limits (MVP, not production)
- No encryption — room/guest data is public on whatever relays you publish to. Don't put anything sensitive in the guest name field as-is; a real deployment would want this behind NIP-44 encryption (e.g. gift-wrapped to staff pubkeys only) rather than plaintext content.
- No access control — anyone with the property's relay+propertyId and a Nostr key can publish room-status events claiming to be "the property." The app only reads events authored by the logged-in pubkey, so a single property today is really "whoever is logged in," not a real multi-staff permission model.
- No history view beyond the last 50 log notes kept in memory per session.