Skip to content

Relay membership ​

These kinds model relay-level membership, Flotilla's notion of joining a relay or space. Rooms are hosted on a relay and scoped by an h tag, while 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 render() throws unless the writer's relays were set with forceRoutes(...routes) (or setRoom(url, room)). See Forced routes 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), rolesFor(pk), rolesByPubkey()addPubkey(pk, roles?), removePubkey(pk), setPubkeys(pks)

Each exported constant is a KindFactory bundling a reader and writer class (RelayJoinReader/RelayJoinWriter, RelayMembersReader/RelayMembersWriter, …). The examples below reach them through the @welshman/app Domain plugin.

Joining and leaving ​

A RelayJoin carries an optional claim code (the claim tag) and a free-text reason (the event content). The RelayJoin and RelayLeave writers mark their events NIP-70 protected (-).

typescript
import {relay} from "@welshman/util"
import {RelayJoin, RelayLeave} 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 is published to the relay being joined:
const writer = domain.writer(RelayJoin)
  .forceRoutes(relay(relayUrl))  // required — RelayJoin sets requiresRelays
  .setClaim(inviteCode)          // ["claim", inviteCode]
  .setReason("hello")            // becomes the content

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

// Leaving has no fields, but still publishes to the relay.
const leave = domain.writer(RelayLeave).forceRoutes(relay(relayUrl))
await (await domain.command(leave)).publish()

RelayInvite (kind 28935) carries just a claim:

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

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

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

Member management ​

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

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

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

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

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

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

RelayMembers (13534) is the member-list snapshot. Members are carried in NIP-43 member tags, ["member", pubkey, ...roleIds], and the writer marks the event NIP-70 protected (-), as NIP-43 requires.

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

const members = domain.reader(RelayMembers)(event)
members.pubkeys()          // string[]
members.isMember(pubkey)   // boolean
members.rolesFor(pubkey)   // string[] of role ids
members.rolesByPubkey()    // Map<pubkey, role ids>

const writer = domain.writer(RelayMembers)
  .forceRoutes(relay(relayUrl))
  .addPubkey(pubkeyA, ["admin"]) // replaces roles; omit to keep existing ones. Also removePubkey(pk) / setPubkeys(pks)

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

See also ​

  • Readers & Writers — the base EventReader/EventWriter pattern.
  • Rooms — NIP-29 room-level membership and metadata ops.