NIP-ROSTER

September 5, 2026 · View on GitHub

Named-Group Roster

draft optional

This NIP defines an addressable Nostr event for publishing a named roster — a list of pubkeys with role and display-name annotations — that other apps can subscribe to for authorisation, membership, or directory purposes.

Motivation

Many apps need to publish "who is on the team" in a way that other apps can read and trust:

  • A football club publishes its stewards so a gate verification app knows whose signed events to accept.
  • An identity app publishes its trusted verifiers so credential-checking sites know which professional verifications to honour.
  • A DAO publishes its core contributors so a treasury tool knows which signing keys to weight.
  • A mutual-aid group publishes its dispatch coordinators so volunteers know who to take instructions from.

The shared shape: one publisher (the "group"), N listed pubkeys, each with a role and a human-readable name, replaceable as the membership changes.

NIP-29 (Simple Groups) covers a richer pattern (relay-managed group state, members, admins, threads) but requires NIP-29-aware relay software. There is no lightweight "just publish a list" event in the existing NIP space.

This NIP defines that lightweight event. It is intentionally minimal: one event, one tag schema, no relay-side semantics.

Why not NIP-29 (Simple Groups)?

NIP-29 requires a relay that implements the full group protocol (kinds 39000–39003 are relay-signed metadata, group membership is mutated by relay-mediated commands). It binds membership to a specific relay's view of the group.

NIP-ROSTER is publisher-signed, relay-agnostic, and addressable per NIP-01 — any standard relay can store it, any client can read it. It does not provide group messaging, moderation, or membership-mutation semantics.

The two NIPs compose: a NIP-29 group's owner could publish a NIP-ROSTER event listing public-facing roles outside the relay-mediated group state.

Why not NIP-51 (Lists)?

NIP-51 lists are about a user's own curated lists (followed, muted, bookmarked). NIP-ROSTER is about a group/organisation's roster of role-holders. Different ownership, different consumption pattern.

A NIP-51 list also lacks per-entry role and display-name annotations.

Why not NIP-02 (Contact List)?

NIP-02 is a single follow list per pubkey, with optional petname. There can only be one per author. NIP-ROSTER allows multiple distinct rosters per author (via the d tag) and richer per-entry metadata.

Event Definition

Kind

31920 — addressable event (range 30000–39999, parameterised replaceable per NIP-01).

The combination of (kind, pubkey, d-tag) uniquely identifies a roster. Publishing a new event with the same triple replaces the previous one.

Structure

{
  "kind": 31920,
  "pubkey": "<group's hex pubkey>",
  "created_at": <unix timestamp>,
  "tags": [
    ["d", "<roster name, ASCII slug>"],
    ["p", "<member pubkey hex>", "<role>", "<display name>"],
    ["p", "<member pubkey hex>", "<role>", "<display name>"]
  ],
  "content": "",
  "id": "<event id>",
  "sig": "<schnorr signature>"
}

Tag Reference

TagStatusFormatPurpose
dREQUIREDASCII slug, max 64 chars, [a-z0-9-]+Roster identifier within this publisher's namespace
pREQUIRED (≥1)[pubkey, role, displayName]One entry per roster member

The p tag uses the four-element form ["p", pubkey, role, displayName]:

  • pubkey — 64 lowercase hex chars (per NIP-01)
  • role — short ASCII slug, max 64 chars, application-defined vocabulary (e.g. gate_steward, safety_officer, verifier, treasurer)
  • displayName — UTF-8 string, max 100 chars, human-readable

The third element (role) and fourth element (displayName) are NIP-ROSTER additions to the standard NIP-02 p tag form.

Roster names

The d tag scopes the roster within a publisher's namespace. A publisher MAY have multiple rosters with distinct d tag values:

  • staff-roster — operational staff
  • verifiers — credential issuers
  • safeguarding — safeguarding officers

Roster names are conventions agreed between publisher and consumer apps. There is no central registry; consuming apps subscribe to the d tag value they expect.

Replacement

Publishing a new kind-31920 event with the same (pubkey, d-tag) replaces the previous one (per NIP-01 addressable event semantics). Removing a member is done by republishing the roster without their p tag. There is no per-entry deletion event.

Verification Flow

sequenceDiagram
    participant G as Group publisher
    participant R as Relay
    participant C as Consumer app
    participant M as Member

    G->>G: Build kind 31920 event<br/>(d, p tags with role + name)
    G->>G: Sign
    G->>R: Publish (replaces previous)
    C->>R: REQ {kinds: [31920], authors: [G.pubkey], '#d': ['staff-roster']}
    R->>C: Latest roster event
    C->>C: Parse p-tags into local cache
    M->>C: Performs role-restricted action (signed by M.pubkey)
    C->>C: Look up M.pubkey in cached roster
    C->>C: Check role permits action
    C->>M: Allow or deny

Validation Rules

IDRule
V-RO-01kind MUST equal 31920
V-RO-02tags MUST contain exactly one d tag
V-RO-03The d tag value MUST match ^[a-z0-9-]{1,64}$
V-RO-04tags MUST contain at least one p tag
V-RO-05Each p tag's pubkey MUST be 64 lowercase hex chars (entries failing this MAY be skipped silently rather than rejecting the whole event)
V-RO-06Each p tag's role, if present, MUST match ^[a-z0-9_-]{1,64}$
V-RO-07Each p tag's displayName, if present, MUST be ≤100 UTF-8 chars and SHOULD have HTML tags stripped at consumption time
V-RO-08Signature MUST verify per NIP-01 (BIP-340 Schnorr over id)
V-RO-09Consumers SHOULD cap roster size at an application-defined maximum (recommended 200 entries) and ignore p-tags beyond the cap

Subscription Filters

Consumer subscribes to a known publisher's roster by name:

["REQ", "roster-sub", {
  "kinds": [31920],
  "authors": ["<publisher hex pubkey>"],
  "#d": ["staff-roster"]
}]

Discovery across publishers (e.g. "all 'verifiers' rosters globally") uses an unconstrained authors filter:

["REQ", "verifiers-discover", {
  "kinds": [31920],
  "#d": ["verifiers"]
}]

Consumers MUST handle EOSE and apply the latest event by created_at per addressable event semantics.

Security Considerations

  • Trust is publisher-bound. A roster is only as authoritative as the publisher's pubkey. Consumers MUST verify the publisher's identity through some out-of-band means (NIP-05, prior agreement, certificate, etc.) before treating the roster as authoritative.
  • Race against revocation. Between a member being removed (republished roster) and the consumer cache being refreshed, the removed member could still pass authorisation checks. Consumers SHOULD subscribe to live updates rather than polling, and SHOULD cap cache age (recommended 1 hour TTL with subscription refresh).
  • Display-name injection. The displayName field is publisher-supplied UTF-8. Consumers MUST sanitise it before display (strip HTML, escape control characters) and SHOULD truncate to a maximum length to prevent UI overflow.
  • Roster size DoS. A publisher could submit a roster with thousands of entries, exhausting consumer memory. Consumers MUST enforce a maximum roster size (V-RO-09) and ignore entries beyond it.
  • Role-name vocabulary collisions. Roles are application-defined. A pubkey listed with role: "admin" in one publisher's roster has no defined relationship to role: "admin" in another's. Consumers MUST scope role checks to the specific publisher whose roster they're consuming.

Dependencies

  • NIP-01 — Basic protocol flow, addressable events, BIP-340 Schnorr signatures.

Test Vectors

Minimal valid roster

{
  "kind": 31920,
  "pubkey": "deadbeef00000000000000000000000000000000000000000000000000000000",
  "created_at": 1745000000,
  "tags": [
    ["d", "staff-roster"],
    ["p", "1111111111111111111111111111111111111111111111111111111111111111", "gate_steward", "Alice"]
  ],
  "content": "",
  "id": "<computed>",
  "sig": "<computed>"
}

Valid multi-role roster

{
  "kind": 31920,
  "pubkey": "deadbeef00000000000000000000000000000000000000000000000000000000",
  "created_at": 1745000000,
  "tags": [
    ["d", "staff-roster"],
    ["p", "1111111111111111111111111111111111111111111111111111111111111111", "gate_steward", "Alice"],
    ["p", "2222222222222222222222222222222222222222222222222222222222222222", "safety_officer", "Bob"],
    ["p", "3333333333333333333333333333333333333333333333333333333333333333", "admin", "Carol"]
  ],
  "content": "",
  "id": "<computed>",
  "sig": "<computed>"
}

Invalid: no d tag

{
  "kind": 31920,
  "tags": [
    ["p", "1111111111111111111111111111111111111111111111111111111111111111", "gate_steward", "Alice"]
  ]
}

Rejected per V-RO-02.

Invalid: no p tags

{
  "kind": 31920,
  "tags": [["d", "staff-roster"]]
}

Rejected per V-RO-04.

Partial: invalid pubkey skipped

{
  "kind": 31920,
  "tags": [
    ["d", "staff-roster"],
    ["p", "not-hex", "gate_steward", "Alice"],
    ["p", "2222222222222222222222222222222222222222222222222222222222222222", "safety_officer", "Bob"]
  ]
}

Per V-RO-05, the not-hex entry is skipped; the event is otherwise valid and Bob is added to the parsed roster.

Reference Implementations

  • Publisher (browser): forgesworn/signet-appsrc/pages/Roster.tsx
  • Parser (Node): forgesworn/matchpass-appserver/roster.js