Define the host-side flow that was previously implicit. A new-room implementer can now go from "I want to start broadcasting" to first audio frame without reading our source. README — new "Hosting a new room" section: - 8-step ASCII walkthrough symmetrical to "Joining sequence". - Covers service/endpoint selection, kind:30312 compose + publish, JWT mint with publish:true, WT/Setup, Announce, presence, Opus stream. - Lists ongoing host duties (add speaker, kick, edit, close, recording). EGG-01 — two new behavior rules: - Rule 12 "Publish-before-mint ordering": peers MUST publish the kind:30312 to relays BEFORE requesting a JWT for it, since the auth sidecar reads the most-recent event by (pubkey, kind, d) to validate existence / status. 410 unknown_room → retry 1s/2s/4s. Closes the silent footgun where a host could mint a token before their event has propagated. - Rule 13 "Service/endpoint selection (host-side guidance)": pre-fill from kind:10112 first entry, fall back to client default with user override, both MUST be https://. EGG-07 — new "Audio publish authorisation" section: - Spells out who gets a publish:true JWT: host (by authorship, implicitly — no need for ["p", _, _, "speaker"] on the host's own p-tag), explicit "speaker" role, or "admin" role. All others 403 publish_forbidden per EGG-02. - Relay does NOT re-read kind:30312; demoting a speaker mid-session does NOT terminate their stream — host MUST kick (kind:4312) to end an active broadcast. https://claude.ai/code/session_01RDpuki4t8StSg1CZcXnV5b
6.0 KiB
EGG-07: Roles & moderation (kind:4312)
status: draft
requires: EGG-01, EGG-04
category: optional
Summary
Hosts and admins manage the room by re-publishing the kind:30312 event
with updated p-tag role markers, and (for kicks) by emitting a separate
kind:4312 admin command. Authorization is signature-based: receivers
gate inbound commands on the signer's role at the time of receipt.
Wire format
Role markers (in kind:30312)
The 4th element of a p-tag, when present, is the role:
["p", "<pubkey>", "<relay hint>", "host" | "admin" | "speaker"]
Effective roles:
| value | privileges |
|---|---|
host |
full control; one host per room (the event author) |
admin |
promote / demote / kick; cannot edit room metadata |
speaker |
may publish audio (EGG-03 put claim required) |
| (omitted) | listener — no publish rights |
Admin command (kind:4312)
{
"kind": 4312,
"pubkey": "<host or admin pubkey hex>",
"tags": [
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"],
["p", "<target pubkey hex>"],
["action", "kick"]
],
"content": "",
...
}
kind:4312 is an ephemeral event — receivers MUST NOT persist it past
the 60-second validity window defined below.
Behavior
Audio publish authorisation
The auth sidecar (EGG-02) MUST grant a publish: true JWT — with the
caller's pubkey hex listed in the JWT's put claim — to any peer that
is one of:
- the host (i.e. the author of the most-recent
kind:30312event for the requested(kind, d)), regardless of whether the host's ownp-tag carries a"host","speaker", or no role marker. The host's speaker capability is implicit in event authorship — clients MUST NOT require the host to add themselves as["p", _, _, "speaker"]in order to broadcast, - a peer marked
["p", <self>, _, "speaker"]in that same event, - a peer marked
["p", <self>, _, "admin"]in that same event.
Other callers requesting publish: true MUST receive HTTP 403
publish_forbidden per the EGG-02 error taxonomy. A publish: false
(listener) request MUST be accepted from any well-formed NIP-98 caller
when the room is open (per EGG-01 rule 6).
The relay (EGG-03 endpoint) is independently authorised by the JWT's
put claim — it does NOT re-read the kind:30312 event. Demoting a
speaker therefore does NOT terminate their existing audio session;
the speaker keeps publishing until either their JWT expires or the
host kicks them via EGG-07's kick command.
Promote / demote
- To promote a listener to speaker, the host or an admin MUST re-publish
the
kind:30312event with the target added (or updated) as["p", <target>, "<relay>", "speaker"]. The new event'screated_atMUST be greater than the previous one. - To demote, the host or admin MUST re-publish the
kind:30312event without the role marker (drop the 4th element of thep-tag) or remove thep-tag entirely. - Hosts MUST NOT demote themselves. Demoting the host (changing the
event author's
p-tag fromhost) is undefined and MAY be ignored by receivers. - The
hostrole is determined by event authorship, not byp-tag marker. The["p", <author>, _, "host"]tag is descriptive only. - Speakers and admins SHOULD NOT delete or edit each other's role markers in their own re-publishes (only the host's re-publish is authoritative, but admins MAY drive promote/demote).
Kick
- To kick a participant, a host or admin signs and broadcasts a
kind:4312event withaction="kick"and ap-tag pointing at the target. - Receivers MUST gate inbound
kind:4312events:- The signer MUST currently hold
hostoradminrole on the activekind:30312(most recentcreated_atper(kind, pubkey, d)). - The event's
created_atMUST be within the last 60 s. - The
a-tag MUST match the room the receiver is in. - The
["action", "kick"]element MUST be present. - The event's
idMUST NOT have been seen in the last 120 s (replay protection — relays may re-deliver the same event from multiple peers, but a single kick MUST act exactly once per receiver). Events failing any gate MUST be silently discarded.
- The signer MUST currently hold
- The TARGET of a kick MUST disconnect from the audio plane (close the moq-lite session) and tear down the in-room UI.
- The kick command does NOT also demote the target. The host MAY follow
up with a
kind:30312re-publish dropping the target's role tag; the client-side filter then prevents future presence events from the target re-rendering them in the participant grid. - The relay MUST NOT enforce kicks. Authorization is purely client-side and signature-based.
Example
Host promotes a listener to speaker:
{
"kind": 30312,
"pubkey": "abc...host",
"created_at": 1714003250,
"tags": [
["d", "office-hours-2026-04"],
["room", "Office Hours"],
["status", "open"],
["service", "https://moq.nostrnests.com"],
["endpoint", "https://moq.nostrnests.com"],
["p", "abc...host", "wss://relay.example", "host"],
["p", "def...co", "wss://relay.example", "speaker"],
["p", "ghi...newSpeaker", "wss://relay.example", "speaker"]
],
"content": "",
"id": "...",
"sig": "..."
}
Admin kicks a disruptive participant:
{
"kind": 4312,
"pubkey": "jkl...admin",
"created_at": 1714003280,
"tags": [
["a", "30312:abchost:office-hours-2026-04"],
["p", "mno...troll"],
["action", "kick"]
],
"content": "",
"id": "...",
"sig": "..."
}
Compatibility
kind:4312 is intentionally a Nostr event, not a moq-lite control message,
so non-broadcasting clients (web, mobile, headless bots) can apply
moderation without speaking to the relay.
Future EGGs MAY introduce additional action values (e.g. "mute",
"warn"). Implementers MUST treat unknown actions as no-ops.