Skip to content

Relay membership

These kinds model relay-level membership — Flotilla's notion of joining a relay/space, distinct from NIP-29 room membership (which is scoped to a room by an h tag). Where Rooms deal with rooms hosted on a relay, these events deal with belonging to the relay itself. They are all plain EventReader / EventWriter subclasses; see Readers & Writers for the base pattern.

Every one of them sets requiresRelays = true, so it must publish to explicit relays — you set them with forceRelays(...urls) (or setRoom(url, room) for the room-scoped kinds) before rendering, and render() throws otherwise. See Forced relays and required relays.

Ops and snapshots

ClassKindPurposeReaderWriter
RelayJoin28934join request (ephemeral)claim(), reason()setClaim(claim), setReason(reason)
RelayInvite28935invite (NIP-29)claim()setClaim(claim)
RelayLeave28936leave marker (ephemeral)
RelayAddMember8000add a memberpubkeys()addPubkey(pk)
RelayRemoveMember8001remove a memberpubkeys()addPubkey(pk)
RelayMembers13534member-list snapshotpubkeys(), isMember(pk)addPubkey(pk, role?), removePubkey(pk), setPubkeys(pks)

Each exported constant is a KindFactory pairing a reader class with a writer class (RelayJoinReader/RelayJoinWriter, RelayMembersReader/RelayMembersWriter, …). You reach the reader/writer through a ConfiguredKind — either RelayJoin.configure(context).reader(event).parse() directly, or, in @welshman/app, via the Domain plugin, which is what the examples below use.

Joining and leaving

A RelayJoin carries an optional claim code (the claim tag) and a free-text reason (the event content):

typescript
import {RelayJoin} from "@welshman/domain"
import {Domain} from "@welshman/app"

const domain = app.use(Domain)

const join = domain.reader(RelayJoin)(event)
join.claim()      // string | undefined — the "claim" tag value
join.reason()     // event.content, or undefined when empty

// A join must be published to the relay/space being joined:
const writer = domain.writer(RelayJoin)
  .forceRelays(relayUrl)       // required — RelayJoin sets requiresRelays
  .setClaim(inviteCode)        // ["claim", inviteCode]
  .setReason("hello")          // becomes the content

const command = await domain.command(writer)
await command.publish()

// Leaving is a bare marker — no fields, but still publishes to the relay.
const leave = domain.writer(RelayLeave).forceRelays(relayUrl)
await (await domain.command(leave)).publish()

RelayInvite (kind 28935) is the invite counterpart, carrying just a claim:

typescript
import {RelayInvite} from "@welshman/domain"

const invite = domain.writer(RelayInvite)
  .forceRelays(relayUrl)
  .setClaim(inviteCode)

await (await domain.command(invite)).publish()

Member management

RelayAddMember (8000) and RelayRemoveMember (8001) are admin ops that list affected pubkeys via addPubkey (deduped by pubkey):

typescript
import {RelayAddMember, RelayRemoveMember} from "@welshman/domain"

const add = domain.writer(RelayAddMember)
  .forceRelays(relayUrl)
  .addPubkey(pubkeyA)
  .addPubkey(pubkeyB)

await (await domain.command(add)).publish()

const remove = domain.writer(RelayRemoveMember)
  .forceRelays(relayUrl)
  .addPubkey(pubkeyA)

await (await domain.command(remove)).publish()

RelayMembers (13534) is the resulting member-list snapshot. Members are carried in NIP-43 member tags, and the writer marks the event NIP-70 protected (-) in its constructor. Its reader answers membership questions; its writer supports add, remove, and a full replace:

typescript
import {RelayMembers} from "@welshman/domain"

const members = domain.reader(RelayMembers)(event)
members.pubkeys()          // string[]
members.isMember(pubkey)   // boolean

const writer = domain.writer(RelayMembers)
  .forceRelays(relayUrl)
  .addPubkey(pubkeyA, "admin")   // optional role; also removePubkey(pk) / setPubkeys(pks)

await (await domain.command(writer)).publish()

See also