Files
welshman/docs/app/data.md
T
hodlbod 5b8fef5b23
tests / tests (push) Failing after 5m8s
Fix NIP conformance in domain kinds; add domain docs/skill
NIP fixes:
- RelayMembers (13534): use NIP-43 `member` tags (not `p`) and set the required
  NIP-70 `-` protected tag.
- Profile (kind 0): remove display-name support entirely (getter, setter, display()
  fallback, and the search weight).
- Comment (1111): A/a tags now carry a real address, not the event id.
- BlossomServerList (10063): normalize server URLs with normalizeUrl (HTTP), not
  normalizeRelayUrl (which forced wss://).
- HandlerRecommendation (31989): fix inverted removeRecommendation filter; add
  setSupportedKind()/supportedKind() for the NIP-89 d-tag.
- Report (1984): place the report-type string on the e tag (note reports) or p tag
  (profile reports); always emit the p tag.

Docs/skills:
- Add @welshman/domain docs (docs/domain/) and the welshman-domain skill.
- Prune @welshman/util docs/skill of the moved Profile/List/Handler/Encryptable
  helpers; register domain in the sidebar, index, and skills README.
- Apply accuracy fixes to the @welshman/app docs/skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsMjvv7krpZeHK1Njeneru
2026-06-20 14:55:21 +00:00

6.1 KiB

Data Plugins

These plugins expose reactive collections of nostr data. They all follow the plugin patterns: read synchronously with get(key), reactively with one(key) (which lazily loads), and use the convenience accessors that return a Projection. Resolve each with app.use(...).

Most event-backed plugins load via the outbox model: they first resolve the author's NIP-65 write relays (from RelayLists), then query those relays. This is why nearly every data plugin depends on relay lists.

Profiles

Kind-0 profiles keyed by pubkey.

const profiles = app.use(Profiles)

profiles.one(pubkey)              // Readable<Maybe<Profile>> — lazily loads
profiles.get(pubkey)             // Maybe<Profile> — sync snapshot, no load
await profiles.load(pubkey)      // explicit load (cached)
profiles.display(pubkey)         // Projection<string> — display name (falls back to npub)
await profiles.publish(values)   // merge a partial values record over the current profile and publish (kind 0)

profiles.display(pubkey).$ is the right thing to bind in a component for a user's name.

Follows

Kind-3 follow lists keyed by pubkey.

const follows = app.use(FollowLists)

follows.one(pubkey)                          // Readable<Maybe<FollowList>>
await follows.follow(["p", otherPubkey])     // add a tag and publish to outbox
await follows.unfollow(otherPubkey)          // remove and publish

Mutes

Kind-10000 mute lists keyed by pubkey. Private entries are NIP-44 encrypted, so decoding is asynchronous.

const mutes = app.use(MuteLists)

mutes.one(pubkey)                            // Readable<Maybe<MuteList>>
await mutes.mutePublicly(["p", pubkey])      // public mute
await mutes.mutePrivately(["p", pubkey])     // encrypted mute
await mutes.unmute(pubkey)
await mutes.setMutes({publicTags, privateTags})

Pins

Kind-10001 pin lists keyed by pubkey.

const pins = app.use(PinLists)

pins.one(pubkey)
await pins.pin(["e", eventId])
await pins.unpin(eventId)

Relay lists

The NIP-65 relay list (kind 10002) is the routing substrate the whole outbox model depends on.

const relayLists = app.use(RelayLists)

relayLists.urls(pubkey)          // Projection<string[]> — all relays
relayLists.readUrls(pubkey)      // Projection<string[]> — read relays
relayLists.writeUrls(pubkey)     // Projection<string[]> — write relays

// Mutations for the current user
await relayLists.addRelay(url, RelayMode.Write)
await relayLists.removeRelay(url, RelayMode.Read)   // also notifies the removed relay
await relayLists.setReadRelays(urls)
await relayLists.setWriteRelays(urls)
await relayLists.setRelays(tags)

Specialized relay lists

Each of these is a separate kind with the same shape (urls(pubkey), addUrl, removeUrl, setUrls):

Plugin Kind Purpose
BlockedRelayLists 10006 Relays the user refuses to connect to (also gates auth and relay quality)
MessagingRelayLists 10050 NIP-17 DM inbox relays (used by gift-wrapped publishing)
SearchRelayLists 10007 NIP-50 search relays
app.use(BlockedRelayLists).urls(pubkey)      // Projection<string[]>
app.use(MessagingRelayLists).urls(pubkey)
app.use(SearchRelayLists).urls(pubkey)

Relays (NIP-11)

Relay metadata fetched over HTTP, keyed by relay URL.

const relays = app.use(Relays)

relays.one(url)                              // Readable<Maybe<RelayProfile>> — lazily fetches NIP-11
relays.display(url)                          // Projection<string>
await relays.hasNip(url, 50)                 // boolean — does the relay support a NIP?
await relays.hasNegentropy(url)              // boolean — NIP-77 / negentropy support

Relay management (NIP-86)

await app.use(RelayManagement).post(url, managementRequest)

Builds a NIP-98 HTTP-auth event signed by the current user and sends a NIP-86 management request to the relay.

Handles (NIP-05)

NIP-05 identifiers verified over HTTP, keyed by name@domain. Lookups are batched (and use dufflepudUrl if configured).

const handles = app.use(Handles)

handles.forPubkey(pubkey)                    // Projection<Maybe<Handle>> — resolves via the profile's nip05
handles.display(nip05)                       // string — displayable nip05
await handles.loadForPubkey(pubkey)

Zappers (Lightning)

LNURL zapper info keyed by lnurl, fetched over HTTP.

const zappers = app.use(Zappers)

zappers.forPubkey(pubkey)                                    // Projection<Maybe<Zapper>>
await zappers.validateZapReceipt(zapReceipt, parentEvent)    // Promise<Maybe<Zap>>
zappers.validZapReceipts(zapReceipts, parentEvent)           // Projection<Zap[]>

Blossom servers

Blossom media-server lists (kind 10063) keyed by pubkey.

const list = await app.use(BlossomServerLists).load(pubkey)
app.use(BlossomServerLists).one(pubkey)      // Readable<Maybe<List>>

Topics

Hashtags with usage counts, derived from the repository's tag index.

const topics = app.use(Topics)

topics.all                                   // Readable<Topic[]>  ({name, count})
topics.byName                                // Readable<Map<string, Topic>>

Rooms (NIP-29)

Relay-based group management. Each method builds the relevant room event and publishes it to a single relay as the current user.

const rooms = app.use(Rooms)

rooms.create(relayUrl, roomMeta)
rooms.edit(relayUrl, roomMeta)
rooms.delete(relayUrl, roomMeta)
rooms.join(relayUrl, roomMeta)
rooms.leave(relayUrl, roomMeta)
rooms.addMember(relayUrl, roomMeta, pubkey)
rooms.removeMember(relayUrl, roomMeta, pubkey)

Plaintext

A cache of decrypted content, keyed by event id. Only decrypts events authored by the current user (e.g. your own private list entries or DMs).

const text = await app.use(Plaintext).ensure(event)   // decrypts & caches
const cached = app.use(Plaintext).get(event.id)        // sync read of the cache