NIRC is relay-scoped public chat built on Nostr event kinds 40-44. It follows the IRC model: the relay is chanserv, npubs are identity (no nickserv), and channel owners have absolute authority over membership.
Published once to create a channel. The event ID becomes the channel ID.
{
"kind": 40,
"content": "{\"name\":\"general\",\"about\":\"General chat\",\"invite_only\":true}",
"tags": []
}
Content JSON fields:
name (string, required) — channel display nameabout (string, optional) — descriptionpicture (string, optional) — channel avatar URLinvite_only (boolean, default true) — whether membership requires approvalThe pubkey that signs this event is the channel owner forever.
Published by the channel owner to manage the channel ACL. Each new kind 41 replaces the previous one — it's a full snapshot of the current state.
{
"kind": 41,
"content": "{\"name\":\"general\",\"about\":\"Updated description\",\"invite_only\":true}",
"tags": [
["e", "<channel_id>", "<relay_url>", "root"],
["p", "<mod_pubkey>", "mod"],
["p", "<member_pubkey>", "member"],
["p", "<blocked_pubkey>", "blocked"]
]
}
p-tag roles:
mod — can hide messages (kind 43) and block users (kind 44)member — approved to read and write in invite-only channelsblocked — denied accessThe channel owner is implicitly a mod and does not need to be listed.
Standard chat message in a channel.
{
"kind": 42,
"content": "hello world",
"tags": [["e", "<channel_id>", "<relay_url>", "root"]]
}
The e tag with root marker links the message to its parent channel.
Published by a mod or owner to remove a specific message from the channel.
{
"kind": 43,
"content": "spam",
"tags": [["e", "<message_event_id>", "<relay_url>", "root"]]
}
Content is an optional reason. Only kind 43 events authored by channel mods/owner are authoritative. Clients filter hidden messages from the feed.
Published by a mod or owner to block a user from the channel.
{
"kind": 44,
"content": "disruptive behavior",
"tags": [
["e", "<channel_id>", "<relay_url>", "root"],
["p", "<blocked_pubkey>"]
]
}
Content is an optional reason. Blocked users' messages are hidden client-side. The relay should refuse further kind 42 events from blocked users in that channel.
ChatProvider — state management: channels, messages, notifications, moderation
chat.service.ts — event construction and relay queries for kinds 40-44
ChannelList — sidebar with channels, unread badges, mute indicators
ChannelView — message feed, composer, mute toggle, mod actions
ChannelSettingsPanel — owner/mod panel: invite-only toggle, member/mod/blocked management
CreateChannelDialog — channel creation modal
ChatButton (sidebar) — navigation with unread notification dot
ChatPage — split layout: channel list + active channel view
- nirc:lastSeen:<pubkey> — JSON map of channelId to timestamp
- nirc:muted:<pubkey> — JSON array of muted channel IDs
| Action | Owner | Mod | Member | Non-member |
|---|---|---|---|---|
| Send message | yes | yes | yes | no (invite-only) / yes (open) |
| Hide message | yes | yes | no | no |
| Block user | yes | yes | no | no |
| Add/remove mod | yes | no | no | no |
| Add/remove member | yes | yes | no | no |
| Toggle invite-only | yes | no | no | no |
| Change channel metadata | yes | no | no | no |
The client publishes the intent via event kinds. Full enforcement requires the relay to:
Until relay-side enforcement is implemented, moderation is client-side only (hidden messages are filtered in the UI but still stored on the relay).