Sharing and reading community content in edufeed

Von Edufeed ·

Sharing and reading community content in edufeed

STILL UNDER DEVELOPMENT

A community in edufeed is a Nostr keypair that has published a kind 10222 community definition. Content belongs to a community when the content event carries the community's pubkey in an h tag. Anyone can publish such an event to any relay; what a community page shows is decided by the reader, section by section, against the community's publishing rules. This article describes both halves so another client can share into edufeed communities and render them the same way.

The normative tag grammar lives in the NIP draft "Communikey Community Types & Group Membership" (communikey-groups); this article is the implementer's walkthrough of it.

The mechanism in one line

event with ["h", <community-pubkey>] → published to the community's relays → reader queries {"#h": [<community-pubkey>]} → reader keeps only events whose author (or sharer) passes the section's access rule at read time

Two consequences follow from it:

Three community types

The type is never declared. It is derived from which pointer tags the kind 10222 carries:

Type Derived from Membership Who may read
open neither tag below none (followers only) everyone
moderated ["membership", <group-id>, <wss://group-relay>] the NIP-29 root group's roster everyone
closed ["concord", <area-id>, <wss://relay>] end-to-end encrypted (Concord) members; the public sees only the window

membership and concord must not coexist. A definition carrying both is treated as open.

Everything else on the 10222 is shared by all types:

{
  "kind": 10222,
  "pubkey": "<community-pubkey>",
  "tags": [
    ["r", "wss://relay.example"],
    ["r", "wss://enforced.example", "enforced"],
    ["blossom", "https://blossom.example"],
    ["strict", "content"],

    ["content", "Learning"],
    ["k", "30142"],
    ["access", "role", "publisher"],

    ["content", "Calendar"],
    ["k", "31922"],
    ["k", "31923"],
    ["k", "31924"],

    ["content", "Articles"],
    ["k", "30023"],
    ["access", "members"],

    ["membership", "<root-group-id>", "wss://groups.edufeed.org"]
  ],
  "content": ""
}
Tag Meaning
r Community relay. A third slot enforced marks a relay that promises to store only rule-conforming content; content in the third slot inside a section scopes the relay to that section.
blossom Blossom server for the community's uploads
strict ["strict", "content"]: the section list is exhaustive. Without it the sections are advisory and clients fail open (show every content type).
content Starts a content section; the value is its display name. Tags that follow belong to that section until the next content tag.
k Event kind offered in the section
access Publishing rule of the section (moderated only, see below)
a Legacy gates: 30000:<pubkey>:<d> profile list, 30009:... badge. Read-only, never written by current clients, except the publisher window.

Name, picture and description come from the community pubkey's kind 0.

Following a community: kind 30000

A user's communities are one NIP-51 follow set, kind 30000 with d = communities, one p tag per community pubkey, published to the user's own write relays:

{
  "kind": 30000,
  "pubkey": "<user-pubkey>",
  "tags": [
    ["d", "communities"],
    ["p", "<community-pubkey>"],
    ["p", "<another-community-pubkey>"]
  ],
  "content": ""
}

Joining appends a p tag, leaving removes it, and each change replaces the whole event with a strictly newer created_at. Query a user's communities with {"kinds": [30000], "authors": [<user>], "#d": ["communities"]}.

Following is open to anyone for every community type and is not membership. Membership exists only for moderated communities (the NIP-29 roster) and closed communities (the encrypted roster). The older kind 30382 relationship event is retired; edufeed neither writes nor reads it.

Sharing content into a community

1. Original content: add the h tag

Any event created for a community carries the community pubkey as an h tag, alongside its normal tags:

["h", "<community-pubkey>"]

An event may carry several h tags to target several communities at once. Chat messages (kind 9) and forum threads (kind 11) are the exception: they are single-community and carry exactly one.

2. Existing content: a NIP-18 repost with h tags

To share an event that already exists, publish a repost, kind 6 for a kind 1 note and kind 16 for everything else. The repost references the original with an e tag (plus p for the author) or, for addressable kinds, an a tag, and adds one h tag per target community. One repost may target several communities:

{
  "kind": 16,
  "pubkey": "<sharer-pubkey>",
  "tags": [
    ["a", "30142:<author-pubkey>:<d-tag>", "wss://relay-hint.example"],
    ["k", "30142"],
    ["p", "<author-pubkey>"],
    ["h", "<community-pubkey>"],
    ["h", "<another-community-pubkey>"]
  ],
  "content": ""
}

Leave content empty when an a tag is present. Do not share an event its author has deleted (NIP-09): the repost publishes fine, but nobody can resolve it.

3. Where to publish

A community only sees what reaches the relays it reads from. edufeed publishes community content to the union of:

Reposts additionally go to the app's communikey relays, because community pages look for shares there.

4. Legacy: kind 30222 targeted publications

Older clients targeted content with a separate kind 30222 event (p = community, e/a = content, k = kind). Readers still honour these; nothing writes them any more.

Reading community content

Step 1: load the definition

Fetch the community's kind 10222 and kind 0. Parse the content sections in tag order. If the definition carries ["strict", "content"], offer only the declared sections; otherwise show every content type you support.

Step 2 (moderated only): apply a section override

Root-group admins usually do not hold the community key, so they may reshape the section block with a kind 30223 signed by their own key:

{
  "kind": 30223,
  "pubkey": "<admin-pubkey>",
  "tags": [
    ["d", "<community-pubkey>"],
    ["h", "<community-pubkey>"],
    ["content", "Learning"],
    ["k", "30142"],
    ["access", "role", "publisher"]
  ],
  "content": ""
}

Query {"kinds": [30223], "#d": [<community-pubkey>]} on the communikey app relays and the community's relays (never on the auth-gated group relay: logged-out readers need this to render the tabs). An override is valid only if its d tag equals the community pubkey and its author currently holds a moderation role (admin, king or moderator) in the root group's kind 39001. Among valid overrides the newest created_at wins, ties broken by the lower id, and the winner applies only while it is newer than the kind 10222 itself. It replaces the section block as a whole. Only content, k and access tags are honoured; relays, profile lists, badges and form references on an override are ignored.

Open and closed communities have no roster to check against, so they ignore kind 30223 entirely.

Step 3: query three sources per section

For a section offering kinds K, on the community's relays plus the app relays for those kinds:

{"kinds": K, "#h": ["<community-pubkey>"]}

Reposts live on the sharer's outbox, so query them on a broad relay set (all app relays plus the community relays):

{"kinds": [6, 16], "#h": ["<community-pubkey>"]}

Legacy targeted publications, on the communikey relays:

{"kinds": [30222], "#p": ["<community-pubkey>"], "#k": ["<kind>", "..."]}

Resolve the e/a references of reposts and 30222 events, using their relay hints and the same broad relay set. Do not add an authors restriction to these queries: the h tag is the scoping, and the access rule is applied after loading.

Step 4: resolve the section's allowed authors

Compute, per section, either "everyone" or the set of pubkeys allowed to author it right now (see the table below). Until the roster has arrived, edufeed renders the section unfiltered rather than empty; the composer, by contrast, is conservative while loading and lets only the community key publish.

Step 5: filter

Render an item only if its author is allowed, or, for a repost, if the sharer is allowed. An event carrying several h tags is judged per community and may render in one and be dropped in another.

Access rules per section

Section tag Who may author Source of truth
no access tag everyone —
["access", "members"] the root group's members kind 39002 of the root group, unioned with every pubkey in 39001
["access", "role", "<role>"] members holding <role> kind 39001 entries whose role list contains <role>
["a", "30000:<pubkey>:<d>", <relay>] the p tags of that follow set the referenced kind 30000 event (legacy profile list; also the publisher window)
["a", "30009:<pubkey>:<badge>"] badge holders legacy, read-only

Rules that hold for all of them:

The root group

The membership tag points at a NIP-29 group. Its relay signs three addressable events with d = group id:

Kind Content
39000 group metadata (name, public/private, open/closed)
39001 admins: ["p", <pubkey>, <role>, <role>, ...]
39002 members: ["p", <pubkey>]

NIP-29 has only one place to record a role, kind 39001, so a role that grants nothing beyond publishing lands in the "admins" list next to the real moderators. Read the role names: admin, king and moderator carry moderation authority; publisher is the conventional publish-only role and must never be treated as moderation. A pubkey listed in 39001 is a member even when 39002 omits it.

Joining is a kind 9021 join request to the root group's relay, optionally with an invite code tag; admins accept with a kind 9000 put-user (which replaces the target's whole role set), and members leave with kind 9022. All of these go to the group's host relay only. The application-form join flow described in early drafts was removed; today joining is by invite code or join request.

Removing content from a community

Sometimes an item that passes every access rule still does not belong: a share that landed in the wrong community, an off-topic resource, something a moderator wants gone from this page. Nothing in Nostr lets a moderator delete someone else's event. NIP-09 deletions are author-only (a client must check that the deletion's pubkey matches the target's), and NIP-29's delete-event (kind 9005) only covers events the group relay itself stores under the group's h tag, which community content never is. So edufeed does what Nostr offers for exactly this case, NIP-32 "distributed moderation": whoever runs the community publishes a signed label saying "this item is removed here", and readers honour the label when its author has authority.

The removal label

{
  "kind": 1985,
  "pubkey": "<moderator-or-community-pubkey>",
  "tags": [
    ["L", "communikey.moderation"],
    ["l", "remove", "communikey.moderation"],
    ["h", "<community-pubkey>"],
    ["k", "30023"],
    ["e", "<event-id>", "wss://relay.example/"],
    ["a", "30023:<author-pubkey>:<d-tag>", "wss://relay.example/"]
  ],
  "content": ""
}

Who may remove

A label counts only if, at read time, its author is the community key itself, or, in a moderated community, currently holds a moderation role (admin, king or moderator) in the root group's kind 39001. This is the same rule as for the kind 30223 section override, and for the same reasons: a publisher-only or custom role must not be able to hide content, and judging against the current roster makes removals retroactive. Demote a moderator and everything they removed comes back, with nothing to delete. Open communities have a single authority, the key holder; closed communities too for now.

In edufeed, owners and moderators find "Aus der Community entfernen" in the menu of an item's detail view inside the community (articles, resources, wikis, threads, calendar events). When the acting user holds a moderation role, edufeed signs with their own key, so the label records who acted; the community key is used only when no personal authority exists.

Reading removals

Query the labels alongside the content:

{"kinds": [1985], "#h": ["<community-pubkey>"], "#L": ["communikey.moderation"]}

on the community's relays plus the usual lookup relays (a label also lives on its signer's outbox). Keep the valid ones and drop every item whose id or address a valid label names. Removal labels are ordinary public events, published to the community's relays and the signer's write relays, never only to the auth-gated group relay: the logged-out readers of a community page have to see them too.

Undoing a removal

Restoring an item is the label author's own NIP-09 deletion of the label, which every relay and client already handles. It follows that only whoever removed an item can restore it: a moderator their own removals, the key holder the community-signed ones. edufeed lists all current removals under "Entfernte Inhalte" in the community settings, with who removed what and when, and offers "Wiederherstellen" where the browser holds the matching key.

Closed communities and the publisher window

A closed community's 10222 is a discoverable shell: kind 0 plus the concord pointer, no public content. Everything inside is end-to-end encrypted, and so is the roster, which makes ordinary members/role gating impossible: a reader cannot check an author against a list nobody may see.

The publisher window solves this with a consented public subset of the membership:

  1. Private grant. Inside the Concord area the owner creates a role named Publisher and grants it to members. Nothing leaves the area.

  2. Private consent. A member who agrees to be listed publicly publishes a kind 3320 rumor into the area's guestbook plane with ["t", "publisher-window"] and ["status", "accepted"] (or "revoked"). The newest rumor per member wins; on a same-second tie the lower id wins.

  3. Public list. When both the grant and an accepted consent exist, the community key publishes a follow set listing exactly those pubkeys:

    {
      "kind": 30000,
      "pubkey": "<community-pubkey>",
      "tags": [
        ["d", "publishers"],
        ["p", "<consenting-publisher>"]
      ],
      "content": ""
    }
    

    Revoking either the role or the consent removes the pubkey on the next update. Only the community key can sign this event, so updates land when a key-holding client is online.

  4. Window sections. The owner chooses which content types the window exposes. Each becomes an ordinary section whose only gate is the list:

    ["content", "Learning"],
    ["k", "30142"],
    ["a", "30000:<community-pubkey>:publishers", "wss://relay.example"]
    

    Chat and meet rooms are never window content; the window shares content outward, it does not host conversation.

For a reader nothing is special: a window section is a profile-list-gated section, so steps 3 to 5 above apply unchanged. The public learns only who consented to be listed, never the membership size or anyone else. A community whose public sections are all window sections still counts as closed; it becomes open only if it declares at least one section without the publishers-list gate.

Conversation lanes are separate

Chat (kind 9) and forum (kind 11) are also h-tagged, but they follow different transport rules. In an open community chat messages go to the community's relays like any content. In a moderated community the channels are NIP-29 groups declared with ["group", <id>, <relay>, <name>, <access>] tags on the 10222, and every event for a channel goes to that group's host relay only. This article does not cover the channel lane.

Resolution rules in short

Known limits