docs(nestsClient/specs): EGG-00..EGG-12 — Extensible Gossip Guidelines

Wire-protocol specs for nostrnests-style audio rooms, in the
style of Nostr NIPs and Blossom BUDs. Each spec documents one
self-contained capability that a client or relay can implement;
two compliant peers implementing the same set of EGGs round-trip
without further coordination.

Layout:

  README.md            cover, status table, conformance levels
  EGG-01.md  Room event (kind:30312)              required
  EGG-02.md  Auth + WebTransport handshake        required
  EGG-03.md  Audio plane (moq-lite)               required
  EGG-04.md  Presence (kind:10312)                required
  EGG-05.md  In-room chat (kind:1311)             optional
  EGG-06.md  Reactions (kind:7)                   optional
  EGG-07.md  Roles & moderation (kind:4312)       optional
  EGG-08.md  Scheduling (status=planned)          optional
  EGG-09.md  User server list (kind:10112)        optional
  EGG-10.md  Theming (c/f/bg tags)                decorative
  EGG-11.md  Recording                            decorative
  EGG-12.md  Catalog track (catalog.json)         optional

Conformance levels (Listener / Speaker / Host) defined in the
README so a deployment can declare "we implement EGG-01..EGG-04"
and other peers know exactly what to expect.

Each spec follows the same shape (Summary / Wire format /
Behavior numbered MUST/SHOULD/MAY rules / Example /
Compatibility) and fits on a single printed page. Wire formats
are documented exactly as nostrnests + amethyst implement them
on this branch — no hypothetical capabilities, no "future" tags
without an EGG number.
This commit is contained in:
Claude
2026-04-27 03:54:45 +00:00
parent 538525f003
commit 5c40e27fde
13 changed files with 1284 additions and 0 deletions
+94
View File
@@ -0,0 +1,94 @@
# EGG-01: Room event (`kind:30312`)
`status: draft`
`requires: NIP-01`
`category: required`
## Summary
A nests audio room is a NIP-53 addressable event of `kind:30312`. The author
is the host. Subscribers read this event to learn who runs the room, where the
audio plane lives, who else is invited, and whether the room is currently live.
Every other EGG layers on top of this event.
## Wire format
```json
{
"kind": 30312,
"pubkey": "<host pubkey hex>",
"tags": [
["d", "<room id>"],
["room", "<room name>"],
["summary", "<one-line description>"],
["image", "<optional cover image URL>"],
["status", "open" | "private" | "closed" | "planned"],
["service", "<https URL of moq-auth sidecar>"],
["endpoint", "<https URL of moq-relay WebTransport>"],
["relays", "<wss relay 1>", "<wss relay 2>", ...],
["p", "<pubkey>", "<relay hint>", "host" | "admin" | "speaker"],
...
],
"content": "",
...
}
```
The `d` tag is the room id; `(pubkey, kind, d)` is the addressable identity.
## Behavior
1. Hosts MUST emit exactly one `kind:30312` event per room. Updating the room
(rename, status change, role grants) MUST re-publish the same `d` tag with
a higher `created_at`.
2. The `room`, `status`, `service`, and `endpoint` tags MUST be present. A
client receiving an event without all four MUST treat the room as
un-joinable.
3. The `service` value MUST be the base URL of an EGG-02 auth sidecar (do
not include the `/auth` suffix).
4. The `endpoint` value MUST be the base URL of a WebTransport-capable
moq-relay implementing EGG-03.
5. The `p` tag MUST list the host as the first participant. Additional
`p` tags grant roles (EGG-07).
6. Status semantics:
- `open` — room is live, anyone can join.
- `private` — room is live, an out-of-band invitation gate applies (relay
enforces; client behavior identical to `open`).
- `closed` — room is over, audio plane SHOULD be torn down server-side.
- `planned` — room hasn't started; see EGG-08.
7. A host MAY treat a room as auto-closed after 8 h of `created_at` staleness
even if the published status is still `open`/`private`. Receivers SHOULD do
the same to avoid stale rooms hanging at the top of the live UI.
8. An empty `content` field is REQUIRED. Future EGGs MAY define structured
content; until then, peers MUST ignore non-empty content rather than
rejecting the event.
## Example
```json
{
"kind": 30312,
"pubkey": "abc...host",
"created_at": 1714003200,
"tags": [
["d", "office-hours-2026-04"],
["room", "Office Hours"],
["summary", "Weekly Q&A"],
["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"]
],
"content": "",
"id": "...",
"sig": "..."
}
```
## Compatibility
Peers without EGG-01 cannot interpret any other EGG. EGG-04, EGG-05, EGG-06,
EGG-07, and EGG-12 reference rooms by the `(kind, pubkey, d)` tuple defined
here.
+111
View File
@@ -0,0 +1,111 @@
# EGG-02: Auth & WebTransport handshake
`status: draft`
`requires: EGG-01, NIP-98`
`category: required`
## Summary
To open the audio plane (EGG-03), a peer first proves identity to a per-room
auth sidecar (moq-auth) using a NIP-98 HTTP signature, receives a short-lived
JWT, then opens a WebTransport session against the moq-relay carrying the JWT
in the URL.
Two separate URLs are involved: the `service` tag from EGG-01 is the auth
sidecar; the `endpoint` tag is the relay.
## Wire format
### Step 1 — token request
```
POST <service>/auth
Authorization: Nostr <base64(NIP-98 kind:27235 event)>
Content-Type: application/json
{ "namespace": "nests/<kind>:<host_pubkey_hex>:<room_d>",
"publish": <boolean> }
```
`<kind>` is `30312` for nests audio rooms (EGG-01).
The NIP-98 event MUST bind to this exact URL, method `POST`, and the SHA-256
hash of the request body.
### Step 2 — token response
```
HTTP/1.1 200 OK
Content-Type: application/json
{ "token": "<jwt>" }
```
The JWT carries (at minimum):
| claim | meaning |
|---------|---------------------------------------------------------------|
| `root` | The exact `namespace` echoed back. Authorisation is scoped here. |
| `get` | Read-allowed sub-paths; for nests this is `[""]` (any). |
| `put` | Publish-allowed sub-paths. For listeners: `[]`. For speakers: `[<own pubkey hex>]`. |
| `iat` | Unix seconds; issuance. |
| `exp` | Unix seconds; expiry. MUST be `iat + 600` for nests today. |
### Step 3 — WebTransport CONNECT
```
:method = CONNECT
:protocol = webtransport
:scheme = https
:authority = <host:port from `endpoint`>
:path = /<namespace>?jwt=<token>
```
The relay reads the JWT from the `?jwt=` query string. No `Authorization`
header is used at this step.
## Behavior
1. Listening peers MUST request `publish: false`. Speaking peers MUST request
`publish: true` AND the JWT's `put` claim MUST list the speaker's own
pubkey hex.
2. The auth sidecar's TLS certificate MUST be a publicly-trusted chain. The
relay's TLS chain MAY be self-signed in development; production deployments
MUST use a public chain.
3. The auth sidecar SHOULD reject any request whose NIP-98 `created_at` is
more than 60 s in the past or future.
4. The JWT lifetime is fixed at 600 s. There is no refresh endpoint. A client
that needs a longer session MUST mint a fresh token and open a new
WebTransport session before the old token expires; see the deployment-side
commentary in `nestsClient/plans/`.
5. The relay MUST close the WebTransport session within 30 s of `exp`. A peer
receiving an unexpected close MUST be prepared to mint a fresh token and
reconnect.
6. The relay MUST NOT trust the WebTransport authority for authorisation —
only the JWT's `root` claim. A token issued for namespace A MUST NOT be
accepted on a session opened against namespace B.
7. A peer MUST NOT log the JWT or include it in error reports. The token is a
bearer credential.
## Example
```
> POST https://moq.nostrnests.com/auth
> Authorization: Nostr eyJ...kind27235...
> Content-Type: application/json
>
> {"namespace":"nests/30312:abchost:office-hours-2026-04","publish":false}
< HTTP/1.1 200 OK
< Content-Type: application/json
<
< {"token":"eyJhbGc..."}
> CONNECT :path=/nests/30312:abchost:office-hours-2026-04?jwt=eyJhbGc...
< 200 OK (WebTransport session established)
```
## Compatibility
EGG-03 (audio plane) operates inside the WebTransport session this EGG opens.
EGG-12 (catalog) shares the same session.
+111
View File
@@ -0,0 +1,111 @@
# EGG-03: Audio plane (moq-lite)
`status: draft`
`requires: EGG-02`
`category: required`
## Summary
Audio is carried over [moq-lite](https://github.com/kixelated/moq) Lite-03 on
top of the WebTransport session opened in EGG-02. Each speaker publishes one
broadcast keyed by their pubkey hex, on a single track named `audio/data`,
encoded as Opus.
This EGG defines the broadcast/track naming convention and the audio codec
parameters. It does not redefine moq-lite framing — refer to the upstream
spec for the wire layout of `Subscribe`, `Announce`, `Group`, and `Frame`
control messages.
## Wire format
### Speaker → relay (publish)
A speaker MUST send a moq-lite `Announce` with:
```
prefix = "" (empty — relative to the JWT's `root` namespace)
suffix = <pubkey hex>
status = Active
```
After the announce, the speaker opens a unidirectional QUIC stream per group,
prefixed with a moq-lite `Group` header followed by `Frame`s carrying Opus
payloads.
### Listener → relay (subscribe)
A listener MUST send a moq-lite `Subscribe` with:
```
broadcast = <speaker pubkey hex>
track = "audio/data"
priority = 128 (recommended)
ordered = true
maxLatency = 0 (unlimited)
```
The relay forwards each subsequent `Group` and its `Frame`s to the listener
in subscribe-id order.
### Codec
| Parameter | Value |
|-----------------|---------------------------------------|
| Codec | Opus (RFC 6716) |
| Sample rate | 48 kHz |
| Channels | 1 (mono) |
| Frame duration | 20 ms (960 samples) |
| Bit-rate target | 32 kbit/s (configurable, no upper cap)|
| VBR | Allowed |
A new `Group` MUST be opened on every Opus reset (e.g. mute/unmute).
## Behavior
1. A speaker MUST emit an `Announce` with `status=Active` immediately after
the WebTransport session reaches a usable state, and emit `status=Ended`
when stopping the broadcast cleanly.
2. A speaker MUST cap their broadcast to one concurrent `audio/data` track
per session. Multi-quality stacks are reserved for a future EGG.
3. Listeners MUST tolerate gaps in the group sequence (frames dropped due to
network loss). They MUST NOT request retransmits.
4. Listeners SHOULD subscribe with `ordered=true`. Out-of-order delivery
would re-introduce reorderable jitter the Opus decoder is not equipped to
handle.
5. A speaker MUST encode in mono. Stereo broadcasts are reserved for a future
EGG.
6. Listeners MUST NOT subscribe to their own broadcast (would create an audio
loopback through the relay).
7. The relay MUST drop a publishing peer's `Announce` and any subsequent
stream data if the JWT's `put` claim does not contain `<pubkey hex>`.
## Example
A two-speaker room:
```
A's session: ANNOUNCE prefix="" suffix="speakerA" status=Active
GROUP subscribe-id=…, sequence=0
FRAME <opus payload, 20ms>
FRAME <opus payload, 20ms>
Listener's session:
SUBSCRIBE broadcast="speakerA" track="audio/data"
← GROUP sequence=0
← FRAME <opus payload, 20ms>
← …
SUBSCRIBE broadcast="speakerB" track="audio/data"
← GROUP sequence=0
← FRAME <opus payload, 20ms>
← …
```
## Compatibility
EGG-12 adds an OPTIONAL `catalog.json` track per broadcast carrying codec
metadata. EGG-12 MUST NOT change the `audio/data` track parameters defined
here.
A future "video plane" EGG MAY introduce additional tracks; their names MUST
NOT collide with `audio/data` or `catalog.json`.
+104
View File
@@ -0,0 +1,104 @@
# EGG-04: Presence (`kind:10312`)
`status: draft`
`requires: EGG-01`
`category: required`
## Summary
Each participant in a room periodically broadcasts a `kind:10312` event
declaring their state: which room they are in, whether their hand is raised,
whether they are muted, whether they are actively pushing audio, and whether
they currently hold a stage seat.
Presence is the source of truth for the audience-side participant grid, the
hand-raise queue, the listener counter, and the home-screen "live" bubble.
## Wire format
`kind:10312` is a NIP-01 *replaceable* event with a fixed `d`-tag (one
presence-state per pubkey, regardless of how many rooms a user joins; see
"Behavior" below).
```json
{
"kind": 10312,
"pubkey": "<participant pubkey hex>",
"tags": [
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"],
["hand", "0" | "1"],
["muted", "0" | "1"],
["publishing", "0" | "1"],
["onstage", "0" | "1"],
["alt", "Room Presence tag"]
],
"content": "",
...
}
```
The `a` tag is REQUIRED and points at the room defined in EGG-01.
## Behavior
1. A participant MUST emit a `kind:10312` event:
- on entering the room,
- every 30 s while in the room,
- immediately when any flag (`hand`, `muted`, `publishing`, `onstage`)
changes,
- one final time on leaving, with `publishing="0"` and `onstage="0"`.
2. Receivers MUST treat a participant as departed if no presence has been
observed in `> 6 minutes`. (One missed heartbeat plus a 5 minute
tolerance window.)
3. The `hand` flag is the participant's request-to-speak. Hosts and admins
read it to populate a queue (EGG-07).
4. The `muted` flag is set ONLY by speakers who are also publishing. Pure
audience members SHOULD omit it.
5. The `publishing` flag means "I currently have an open `audio/data`
broadcast on the relay" (EGG-03). It MUST be `0` whenever the speaker
has stopped publishing, regardless of mute state.
6. The `onstage` flag means "I currently hold a speaker slot in the room's
`p`-tag list (EGG-01) AND I have not voluntarily stepped off". A
speaker who steps off the stage without losing the role tag MUST emit
`onstage="0"`.
7. The `d`-tag MUST be the FIXED string `nests-room-presence` (chosen so
that one presence event represents a participant's CURRENT room across
all clients). A participant MUST NOT be in two rooms simultaneously.
8. Every flag MUST be present in every emitted event. Receivers MAY assume
omitted flags default to `0`, but emitters that omit are non-conformant.
9. Hosts and clients displaying the participant grid MUST hide presences
from `kind:10312` events whose `created_at` is older than the staleness
window in rule (2).
## Example
```json
{
"kind": 10312,
"pubkey": "def...co",
"created_at": 1714003231,
"tags": [
["a", "30312:abchost:office-hours-2026-04", "wss://relay.example"],
["hand", "0"],
["muted", "0"],
["publishing", "1"],
["onstage", "1"],
["alt", "Room Presence tag"]
],
"content": "",
"id": "...",
"sig": "..."
}
```
## Compatibility
EGG-07 (moderation) reads `hand` to drive the host's promote queue.
EGG-03 (audio plane) is the source of truth for whether a speaker IS
broadcasting; presence's `publishing` flag is a Nostr-side reflection that
clients use as a hint when they cannot subscribe to the audio plane (e.g.
listeners on weak networks rendering the home bubble).
The `kind:10312` shape here intentionally mirrors `kind:10311` from
NIP-53 streaming so that a generic Nostr client can render audience
indicators for both protocols without code-paths.
+75
View File
@@ -0,0 +1,75 @@
# EGG-05: In-room chat (`kind:1311`)
`status: draft`
`requires: EGG-01`
`category: optional`
## Summary
In-room text chat reuses NIP-53's `kind:1311` "live activities chat message"
event, addressed to the room via an `a`-tag. No new event kind, no new
relay convention.
A peer that does not implement EGG-05 simply does not render or emit chat;
all other EGGs continue to interop.
## Wire format
```json
{
"kind": 1311,
"pubkey": "<author pubkey hex>",
"tags": [
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"]
],
"content": "<plaintext message>",
...
}
```
The `content` field carries the plaintext (UTF-8) message.
## Behavior
1. Senders MUST emit chat events with `kind:1311` and exactly one `a`-tag
pointing at the room (EGG-01).
2. Receivers MUST subscribe to relays declared on the room's `relays` tag
(EGG-01) with filter `{kinds:[1311], "#a":["30312:<host>:<d>"]}`.
3. Receivers MUST de-duplicate by event id when the same message arrives
from multiple relays.
4. Receivers MUST sort the chat panel by `created_at` ascending so the
newest message is at the bottom.
5. Senders MUST NOT include private content. The chat plane is public;
any peer subscribed to the room's relays sees every message.
6. Senders MAY include other NIP tags (`p` mentions, `e` quotes, `q`
embeds). Receivers MAY render these per the relevant NIP; lacking
support MUST NOT cause the message to be hidden.
7. A peer SHOULD NOT publish chat events while `status` of the
`kind:30312` is `closed` — but receivers MUST tolerate them gracefully
in case of a race against the host's close.
## Example
```json
{
"kind": 1311,
"pubkey": "def...co",
"created_at": 1714003205,
"tags": [
["a", "30312:abchost:office-hours-2026-04", "wss://relay.example"]
],
"content": "Hi everyone, glad to be here!",
"id": "...",
"sig": "..."
}
```
## Compatibility
EGG-05 events flow on Nostr relays, not the audio plane. A relay that
implements only the audio half of nests can be paired with any NIP-01
relay to provide the chat half.
EGG-06 (reactions) targets `kind:7` events at the same `a`-tag. Chat and
reactions are independent: a peer MAY render reactions without rendering
chat or vice versa.
+104
View File
@@ -0,0 +1,104 @@
# EGG-06: Reactions (`kind:7`)
`status: draft`
`requires: EGG-01`
`category: optional`
## Summary
Audience-side ephemeral feedback (👏 ❤️ 🎉 ...) is carried on standard
NIP-25 `kind:7` reaction events, scoped to the room via an `a`-tag.
Clients render these as a short-lived floating overlay over the
participant grid.
A reaction is NOT durable state. Clients SHOULD NOT page through
historical reactions; only the recent window matters.
## Wire format
```json
{
"kind": 7,
"pubkey": "<reactor pubkey hex>",
"tags": [
["a", "30312:<host pubkey hex>:<room d>", "<relay hint>"],
["p", "<target pubkey hex>"]?,
["emoji", "<shortcode>", "<image url>"]?
],
"content": "<emoji glyph or :shortcode:>",
...
}
```
`content` carries the reaction itself. Standard Unicode emoji glyphs are
used directly. Custom emoji follow NIP-30: `content = ":shortcode:"` with
a sibling `["emoji", shortcode, url]` tag.
A reaction MAY include a `["p", target]` tag scoping the reaction to a
specific speaker (renders over their avatar). With no `p` tag, the reaction
is room-wide (renders centered on the room canvas).
## Behavior
1. Reactors MUST emit `kind:7` events with at least one `a`-tag matching
EGG-01 and a non-empty `content` field.
2. Receivers MUST apply a 30-second sliding window: a reaction is rendered
from the moment it is observed until 30 seconds after its `created_at`,
then dropped from the floating overlay.
3. Receivers SHOULD throttle their reaction-overlay updates to at most 4 Hz
(250 ms minimum between repaints). At ~30 reactions per second a naive
recomposition can starve the audio decoder.
4. Receivers MUST tolerate unknown `content` values (e.g. a Unicode glyph
without a font fallback) by falling back to a generic indicator rather
than dropping the reaction.
5. Receivers MUST NOT auto-fetch the `["emoji", _, url]` image at the
moment of receipt — that opens an animated-emoji-driven DDoS vector.
Image fetch SHOULD be batched and rate-limited.
6. A reactor MAY include the same `a`-tagged reaction more than once
(e.g. spamming claps). Receivers MUST NOT de-duplicate by content
alone; events with different ids are always distinct.
## Example
A clap reaction targeted at a speaker:
```json
{
"kind": 7,
"pubkey": "ghi...audience",
"created_at": 1714003220,
"tags": [
["a", "30312:abchost:office-hours-2026-04"],
["p", "def...co"]
],
"content": "👏",
"id": "...",
"sig": "..."
}
```
A custom-emoji reaction (NIP-30):
```json
{
"kind": 7,
"pubkey": "ghi...audience",
"created_at": 1714003221,
"tags": [
["a", "30312:abchost:office-hours-2026-04"],
["emoji", "amethyst", "https://example/amethyst.png"]
],
"content": ":amethyst:",
"id": "...",
"sig": "..."
}
```
## Compatibility
EGG-06 is independent from EGG-05 (chat). A peer MAY implement either,
both, or neither without affecting interop on EGG-01 through EGG-04.
Future EGGs MAY introduce paid reactions (`kind:9735` zap receipts at the
same `a`-tag). Implementers SHOULD be prepared to source the reaction
overlay from both `kind:7` and `kind:9735` streams.
+143
View File
@@ -0,0 +1,143 @@
# 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`)
```json
{
"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
### Promote / demote
1. To promote a listener to speaker, the host or an admin MUST re-publish
the `kind:30312` event with the target added (or updated) as
`["p", <target>, "<relay>", "speaker"]`. The new event's `created_at`
MUST be greater than the previous one.
2. To demote, the host or admin MUST re-publish the `kind:30312` event
without the role marker (drop the 4th element of the `p`-tag) or
remove the `p`-tag entirely.
3. Hosts MUST NOT demote themselves. Demoting the host (changing the
event author's `p`-tag from `host`) is undefined and MAY be ignored
by receivers.
4. The `host` role is determined by event authorship, not by `p`-tag
marker. The `["p", <author>, _, "host"]` tag is descriptive only.
5. 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
6. To kick a participant, a host or admin signs and broadcasts a
`kind:4312` event with `action="kick"` and a `p`-tag pointing at the
target.
7. Receivers MUST gate inbound `kind:4312` events:
- The signer MUST currently hold `host` or `admin` role on the active
`kind:30312` (most recent `created_at` per `(kind, pubkey, d)`).
- The event's `created_at` MUST be within the last 60 s.
- The `a`-tag MUST match the room the receiver is in.
- The `["action", "kick"]` element MUST be present.
Events failing any gate MUST be silently discarded.
8. The TARGET of a kick MUST disconnect from the audio plane (close the
moq-lite session) and tear down the in-room UI.
9. The kick command does NOT also demote the target. The host MAY follow
up with a `kind:30312` re-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.
10. The relay MUST NOT enforce kicks. Authorization is purely
client-side and signature-based.
## Example
Host promotes a listener to speaker:
```json
{
"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:
```json
{
"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.
+94
View File
@@ -0,0 +1,94 @@
# EGG-08: Scheduling
`status: draft`
`requires: EGG-01`
`category: optional`
## Summary
A host can announce a room before it opens by publishing a `kind:30312`
with `status="planned"` and a `["starts", <unix-seconds>]` tag. Listing
clients render these as upcoming-room cards; the audio plane stays cold
until the host transitions the room to `open`.
## Wire format
```json
{
"kind": 30312,
"pubkey": "<host pubkey hex>",
"tags": [
["d", "<room d>"],
["room", "<room name>"],
["status", "planned"],
["starts", "<unix seconds>"],
["service", "<...>"],
["endpoint","<...>"],
["p", "<host pubkey>", "<relay hint>", "host"]
// ...other EGG-01 tags
],
"content": "",
...
}
```
The `starts` value is a base-10 ASCII unix timestamp in seconds. Clients
that need finer-than-second precision are out of scope; round to the
nearest second.
## Behavior
1. A planned room MUST carry `status="planned"` AND a `starts` tag with
a non-empty numeric value. A planned room without a `starts` value
MUST be treated as un-renderable (rooms with unknown start times
pollute the listing UI).
2. The `starts` value SHOULD be in the future at the time the event is
published. Receivers MUST tolerate past values: a planned room whose
`starts` is in the past renders as "starts soon" rather than
surfacing the past time.
3. To start a planned room, the host MUST re-publish the same `(kind, d)`
with `status="open"` and a higher `created_at`. The host MAY drop the
`starts` tag at this point (no longer informative).
4. To cancel a planned room, the host MUST re-publish the same
`(kind, d)` with `status="closed"`. The `starts` tag SHOULD be
preserved so receivers can label the cancellation
("Cancelled — was scheduled for ...").
5. Planned rooms MUST NOT mint moq-auth tokens (EGG-02). The auth sidecar
SHOULD reject `POST /auth` requests targeting a `(host, room d)` whose
most-recent `kind:30312` is `status="planned"`.
6. Listing UIs SHOULD sort planned rooms ahead of live ones until 5
minutes before `starts`, then promote the room to the same surface as
live rooms (the host is unlikely to be more than 5 minutes late).
7. A peer MUST NOT use `["expiration", <ts>]` (NIP-40) on a planned
`kind:30312`. The implicit 8-hour staleness window from EGG-01 is
the only auto-close mechanism.
## Example
```json
{
"kind": 30312,
"pubkey": "abc...host",
"created_at": 1714000000,
"tags": [
["d", "monthly-q-and-a"],
["room", "Monthly Q&A"],
["summary", "Bring your questions"],
["status", "planned"],
["starts", "1714172400"],
["service", "https://moq.nostrnests.com"],
["endpoint", "https://moq.nostrnests.com"],
["p", "abc...host", "wss://relay.example", "host"]
],
"content": "",
"id": "...",
"sig": "..."
}
```
## Compatibility
Receivers without EGG-08 see a planned room as `status="planned"` and
SHOULD treat it as un-joinable per EGG-01 rule 6 (status-not-`open`
means non-renderable for live UIs). The room becomes joinable as soon
as the host re-publishes with `status="open"`.
+79
View File
@@ -0,0 +1,79 @@
# EGG-09: User server list (`kind:10112`)
`status: draft`
`requires: NIP-01`
`category: optional`
## Summary
A user MAY publish a list of nests servers they prefer to host on. Clients
read this list to default-fill the "service" and "endpoint" fields when the
user opens a new room, and to suggest peer rooms in discovery surfaces.
This is the audio-rooms equivalent of the user-server-list patterns in
Blossom (BUD-03) and Mostr.
## Wire format
`kind:10112` is a NIP-01 *replaceable* event with one entry per server:
```json
{
"kind": 10112,
"pubkey": "<user pubkey hex>",
"tags": [
["server", "<https URL — moq-auth/moq-relay base>"],
["server", "<https URL>"],
...
],
"content": "",
...
}
```
Each `["server", url]` is one entry. The relative ordering MUST be
preserved by receivers — earlier entries are higher priority.
## Behavior
1. Each `server` value MUST be a fully-qualified URL beginning with
`https://`. Receivers MUST reject (drop) entries that are not
well-formed HTTPS URLs.
2. Receivers MUST de-duplicate entries by exact-string match after
trimming a single trailing `/`. Order of remaining entries MUST be
preserved (the FIRST occurrence wins).
3. When the user opens the create-room sheet, the client SHOULD pre-fill
the EGG-01 `service` and `endpoint` fields from the FIRST entry in
the list (a single nests deployment serves both via the same base
URL today).
4. Users MAY enumerate up to 64 servers. Receivers MUST tolerate longer
lists by truncating to the first 64.
5. Hosts who list a server in their `kind:10112` are not declaring an
alliance — they are merely advertising "I will probably create rooms
here". Receivers MUST NOT use the list as a moderation signal.
6. The list is purely a defaults / discovery hint. A `kind:30312` event's
own `service` / `endpoint` tags are authoritative for that specific
room and override the user list at join time.
## Example
```json
{
"kind": 10112,
"pubkey": "abc...host",
"created_at": 1714003000,
"tags": [
["server", "https://moq.nostrnests.com"],
["server", "https://moq.example.org"]
],
"content": "",
"id": "...",
"sig": "..."
}
```
## Compatibility
A peer that does not implement EGG-09 simply does not pre-fill server
fields and does not surface "rooms hosted by this user are usually on…"
hints in profile screens. All other interop is unaffected.
+110
View File
@@ -0,0 +1,110 @@
# EGG-10: Theming
`status: draft`
`requires: EGG-01`
`category: decorative`
## Summary
A host MAY skin their room with custom colors, font, and a background
image. Theme tags are added to the existing `kind:30312` event; clients
without theme support ignore them and render with their default theme.
This EGG is decorative: theming MUST NOT affect protocol semantics
(joining, audio, presence, chat). A non-themed renderer MUST NOT show
fewer features than a themed one.
## Wire format
Three optional tags on `kind:30312`:
```json
["c", "<hex color>", "background" | "text" | "primary"]
["f", "<font family>", "<optional font URL>"]
["bg", "<background image URL>", "cover" | "tile"]
```
### Color (`c`)
- The hex value is six hex digits (`#RRGGBB`); a leading `#` is OPTIONAL.
Three-digit shorthand (`#abc`) is NOT supported.
- The third element selects the role: `background`, `text`, or `primary`.
- A room MAY include up to one `c` tag per role. Extras MUST be ignored
by receivers (palette fallbacks are reserved for a future EGG).
### Font (`f`)
- The family name is a free-form string. Receivers SHOULD match the
four CSS-style generic names case-insensitively: `sans-serif`,
`serif`, `monospace`, `cursive`. Other names fall through to the
platform default unless the renderer also supports the URL.
- The URL element is OPTIONAL. When present, it MUST be a fully-qualified
HTTPS URL pointing at a single-style font file (TTF/OTF/WOFF2). HTTP
URLs MUST be rejected (mixed-content + integrity concerns).
### Background (`bg`)
- The URL is a fully-qualified HTTPS URL pointing at an image (PNG/JPEG/
WEBP).
- The mode is `cover` (scale to fill, crop overflow) or `tile` (repeat
on both axes). Unknown modes MUST fall back to `cover`.
## Behavior
1. Theme tags are PARSED ONLY by clients with rendering support. Clients
without that support MUST ignore them silently.
2. Color values MUST be parsed strictly: `^#?[0-9a-fA-F]{6}$`. Anything
else MUST result in the role falling back to the platform default,
not in a render error.
3. The `text` color is the foreground for room name, summary, and chat
bodies. The `background` color paints behind everything except the
`bg` image (image overlays the color). The `primary` color drives
accent surfaces (active-speaker ring, send button).
4. Font URL fetching MUST be opt-in to the renderer's image / asset
pipeline (Tor or proxy if so configured). Renderers MUST cache the
fetched font on disk keyed by URL hash; the same URL MUST NOT be
re-fetched in the same session.
5. Background image fetching MUST go through the same channel as
inline images (typically the renderer's image cache).
6. Themed renderers MUST tolerate a TIME-OF-FETCH gap: the room screen
opens with platform defaults, font / background swap in once
loaded. There MUST be no flash-of-blank-screen.
7. A room theme MUST NOT alter element positions, sizes, or behavior.
This EGG controls colors, font family, and a background image — no
layout primitives.
## Example
```json
{
"kind": 30312,
"pubkey": "abc...host",
"tags": [
["d", "purple-room"],
["room", "Purple"],
["status", "open"],
["service", "..."],
["endpoint", "..."],
["p", "abc...host", "wss://relay", "host"],
["c", "#1a0033", "background"],
["c", "#FFFFFF", "text"],
["c", "#a020f0", "primary"],
["f", "Inter", "https://fonts.example/inter.woff2"],
["bg", "https://example.com/stars.png", "tile"]
],
"content": "",
"...": "..."
}
```
## Compatibility
A non-themed receiver renders the room without any of the cosmetic
overrides. A themed receiver that fails to fetch one of the assets
(font / background) MUST render the rest of the theme and the room is
still fully functional.
Future EGGs MAY introduce per-user profile themes (e.g. `kind:16767`)
and shared theme references (e.g. `kind:36767` Ditto themes). Those
specs MUST be additive: a theme defined on the room itself overrides
any per-user or shared theme.
+84
View File
@@ -0,0 +1,84 @@
# EGG-11: Recording
`status: draft`
`requires: EGG-01`
`category: decorative`
## Summary
When a host captures a room out-of-band and wants to make the recording
available to audience members who missed the live session, they re-publish
the closed `kind:30312` with a `["recording", url]` tag.
The recording itself is not delivered through the audio plane (EGG-03) —
it is a static asset on the open web. Clients hand the URL to the system
media player rather than embedding playback inline.
## Wire format
A `["recording", url]` tag added to a `kind:30312` event:
```json
["recording", "<https URL of the recording>"]
```
The URL MUST point at a publicly-fetchable audio file (Opus, MP3, AAC, M4A,
or any format the local OS / app ecosystem supports for `audio/*` MIME
types).
A `kind:30312` carrying a `recording` tag MUST also carry `status="closed"`.
A live or planned room with a recording tag is non-conformant; receivers
SHOULD ignore the recording tag in that case.
## Behavior
1. The host MUST NOT publish a `recording` tag until the recording is
uploaded and reachable. A recording tag pointing at a 404 erodes
trust; receivers SHOULD silently hide the listen-back UI when the
URL fails to resolve.
2. The URL MUST be `https://`. `http://` URLs MUST be rejected.
3. The host SHOULD include a `Content-Type` header on the asset's HTTP
response so the system media player can pick the right handler.
4. Receivers SHOULD render the listen-back affordance as a single button
("Listen to recording") that hands the URL to the platform's media
intent (Android: `ACTION_VIEW`; iOS: `UIApplication.open`; web:
`<a target="_blank">`). Receivers MUST NOT auto-play.
5. Receivers MUST gracefully tolerate the absence of any registered
media handler — show the user a "no app installed to play this"
toast rather than crash or no-op silently.
6. Clients MUST NOT subscribe to the audio plane (EGG-03) for a closed
room. The recording is the only audio path post-close.
7. Hosts MAY publish multiple recording tags (e.g. one MP3, one Opus).
Receivers SHOULD prefer the FIRST entry and treat extras as
alternatives.
## Example
```json
{
"kind": 30312,
"pubkey": "abc...host",
"created_at": 1714010000,
"tags": [
["d", "office-hours-2026-04"],
["room", "Office Hours"],
["status", "closed"],
["service", "https://moq.nostrnests.com"],
["endpoint", "https://moq.nostrnests.com"],
["p", "abc...host", "wss://relay", "host"],
["recording", "https://recordings.example.com/office-hours-2026-04.opus"]
],
"content": "",
"...": "..."
}
```
## Compatibility
Receivers without EGG-11 see a closed room without a listen-back button.
A receiver with EGG-11 but without a registered media app for the URL's
MIME type falls back to the toast described in rule 5.
Recording is not part of the moq-lite spec; it is a Nostr-side
augmentation. A nests deployment that does not capture rooms continues
to interop without changes.
+116
View File
@@ -0,0 +1,116 @@
# EGG-12: Catalog track (`catalog.json`)
`status: draft`
`requires: EGG-03`
`category: optional`
## Summary
A speaker MAY publish a sibling `catalog.json` track on the same moq-lite
broadcast as their `audio/data` track. The catalog carries codec
metadata: format, sample rate, channel count, bitrate, etc. It exists so
clients can render a "speaker is broadcasting Opus 48 kHz mono" tooltip
without parsing audio frames, and so future codec migrations can be
negotiated client-side.
## Wire format
### Track convention
A speaker MUST publish the catalog on the SAME broadcast as their audio
track:
```
SUBSCRIBE broadcast=<speaker pubkey hex> track="catalog.json"
```
### Payload
Each `Group` carries a single JSON document encoded as UTF-8. The
publisher emits a fresh group whenever the catalog changes (e.g. codec
swap on mute / unmute). Receivers SHOULD read only the most recent
group.
Document shape:
```json
{
"version": 1,
"audio": [
{
"track": "audio/data",
"codec": "opus",
"sample_rate": 48000,
"channel_count": 1,
"bitrate": 32000
}
]
}
```
Field reference:
| field | type | required | meaning |
|-----------------|--------|----------|--------------------------------------|
| `version` | int | required | Schema version. Currently `1`. |
| `audio` | array | required | One entry per audio track. May be empty. |
| `audio[].track` | string | required | Track name; matches the moq-lite track this catalog describes (typically `"audio/data"`). |
| `audio[].codec` | string | optional | Lower-case codec id (`opus`, `aac`, …). |
| `audio[].sample_rate` | int | optional | Sample rate in Hz. |
| `audio[].channel_count` | int | optional | 1 = mono, 2 = stereo. |
| `audio[].bitrate` | int | optional | Target bitrate in bits per second. |
Unknown fields MUST be tolerated: parsers MUST ignore keys they do not
recognise rather than rejecting the document. New keys are added without
incrementing `version` unless they change the meaning of an existing key.
## Behavior
1. Publishing a catalog is OPTIONAL. A speaker that omits the
`catalog.json` track MUST still be playable per EGG-03 (which already
pins the audio parameters).
2. A subscriber MUST tolerate the absence of a catalog. A `Subscribe`
response of `Drop`/error on the `catalog.json` track is benign — the
subscriber falls back to the EGG-03 default parameters.
3. A subscriber MUST tolerate malformed JSON, missing `version`, and
unknown fields. Failure to parse means "no catalog metadata" — render
the default tooltip.
4. A publisher MUST NOT use the catalog track to deliver authoritative
stream parameters that contradict EGG-03. The catalog is INFORMATIVE,
not normative: a listener MUST be able to decode `audio/data` without
reading the catalog.
5. Subscribers SHOULD limit how often they re-render the catalog tooltip:
at most once per second, even if catalogs are published more
frequently.
6. Hosts MAY use the catalog to detect non-conformant publishers (e.g. a
broadcaster claiming `aac` when EGG-03 mandates `opus`) for
moderation purposes. The actual moderation flow is out of scope for
this EGG.
## Example
A speaker comes onstage with default Opus parameters:
```
SUBSCRIBE broadcast="speakerA" track="catalog.json"
← GROUP sequence=0
← FRAME {"version":1,"audio":[{"track":"audio/data","codec":"opus","sample_rate":48000,"channel_count":1,"bitrate":32000}]}
```
The same speaker bumps to 64 kbit/s:
```
← GROUP sequence=1
← FRAME {"version":1,"audio":[{"track":"audio/data","codec":"opus","sample_rate":48000,"channel_count":1,"bitrate":64000}]}
```
## Compatibility
EGG-12 is purely additive on top of EGG-03. A receiver that does not
support EGG-12 MUST still subscribe to `audio/data` directly (EGG-03)
and decode at the parameters EGG-03 mandates.
A future EGG MAY widen the catalog schema to describe multiple audio
quality tiers, video tracks, or speaker-side hints (e.g. push-to-talk
state). Those additions MUST keep `version: 1` working for existing
parsers; breaking changes ship as `version: 2` with a separate EGG.
+59
View File
@@ -0,0 +1,59 @@
# EGGs — Extensible Gossip Guidelines
Wire-protocol specs for nostrnests-style audio rooms. Each EGG defines one
self-contained capability that a client or relay can implement. Two compliant
peers that implement the same set of EGGs round-trip without further
coordination.
Specs intentionally mirror the [Nostr NIP](https://github.com/nostr-protocol/nips)
and [Blossom BUD](https://github.com/hzrd149/blossom) formats: ASCII Markdown,
RFC-2119 keywords (MUST / SHOULD / MAY), one capability per file.
## Index
| # | File | Topic | Category |
|----|----------------|-------------------------------------------|------------|
| 01 | [EGG-01](./EGG-01.md) | Room event (`kind:30312`) | required |
| 02 | [EGG-02](./EGG-02.md) | Auth & WebTransport handshake | required |
| 03 | [EGG-03](./EGG-03.md) | Audio plane (moq-lite) | required |
| 04 | [EGG-04](./EGG-04.md) | Presence (`kind:10312`) | required |
| 05 | [EGG-05](./EGG-05.md) | In-room chat (`kind:1311`) | optional |
| 06 | [EGG-06](./EGG-06.md) | Reactions (`kind:7`) | optional |
| 07 | [EGG-07](./EGG-07.md) | Roles & moderation (`kind:4312`) | optional |
| 08 | [EGG-08](./EGG-08.md) | Scheduling (`status=planned`) | optional |
| 09 | [EGG-09](./EGG-09.md) | User server list (`kind:10112`) | optional |
| 10 | [EGG-10](./EGG-10.md) | Theming (`c`/`f`/`bg`) | decorative |
| 11 | [EGG-11](./EGG-11.md) | Recording (`recording` tag) | decorative |
| 12 | [EGG-12](./EGG-12.md) | Catalog track (`catalog.json`) | optional |
## Conformance levels
A peer claims **Listener compliance** when it implements EGG-01, EGG-02, EGG-03
and at least the read side of EGG-04.
A peer claims **Speaker compliance** when it implements all of the above plus
the publish side of EGG-03 and the write side of EGG-04.
A peer claims **Host compliance** when it implements Speaker compliance plus
EGG-07 and the write side of EGG-08 (if scheduled rooms are exposed in its UI).
EGG-05 through EGG-12 are independently optional. Lacking any of them MUST NOT
break interop on the EGGs a peer does implement.
## Versioning
Each spec carries a `status` line at the top: `draft` (subject to change),
`accepted` (frozen except for clarifications), or `replaced-by: EGG-XX`
(superseded). Breaking changes ship as a new EGG number; existing numbers are
never re-purposed.
## Naming
EGG = Extensible Gossip Guideline. The acronym is intentional: nostrnests
serves nests; nests hold eggs.
## Filing changes
Edit a single EGG per pull request. Include a wire-format example showing the
delta. If a change crosses two specs, file two PRs and reference each from the
other.