Wikifreedia
All versions

A single event declares a private group’s relay, blossom servers, and content sections. All content is end-to-end encrypted using a rotating epoch key held only by members.

The relay is keyless. It never holds or derives any encryption key. Since content is encrypted, a private group can run on any standard relay; a dedicated relay with AUTH is strongly recommended to prevent metadata leakage but is not required.

Private Group Creation Event (kind:10444)

A private group is created by publishing a kind:10444 event. The pubkey of this key pair is the unique identifier for the group, identical to the public community model.

{
  "id": "<event-id>",
  "pubkey": "<group-pubkey>",
  "created_at": 1675642635,
  "kind": 10444,
  "tags": [
    // recommended: relay requires AUTH and only stores events from profile-list members
    ["r", "<relay-url>", "enforced"],

    // current epoch number and its public key (see Epoch Key System)
    // points to the current kind:30444 epoch announcement
    ["epoch", "1", "<epoch_1_pub>"],

    // one or more blossom servers
    ["blossom", "<blossom-url>"],

    // Interactions section — comments and reactions on group content
    ["content", "Interactions"],
    ["k", "1111"], // comments
    ["k", "7"],    // reactions
    ["k", "1985"], // labels
    ["a", "30000:<pubkey>:Interactions", "<relay-url>"],

    // Chat section
    ["content", "Chat"],
    ["k", "9"],
    ["a", "30000:<pubkey>:Chat", "<relay-url>"],

    // Forum section
    ["content", "Forum"],
    ["k", "11"],
    ["a", "30000:<pubkey>:Forum", "<relay-url>"],

    // Projects section
    ["content", "Projects"],
    ["k", "30315"], // projects
    ["k", "30316"], // milestones
    ["a", "30000:<pubkey>:Projects", "<relay-url>"],

    // Apps section
    ["content", "Apps"],
    ["k", "32267"], // apps
    ["k", "30063"], // releases
    ["a", "30000:<pubkey>:Apps", "<relay-url>"]
  ],
  "content": "",
  "sig": "<signature>"
}

Additional Tag Definitions

Tag Description
epoch Current epoch: ["epoch", "<epoch-number>", "<epoch_pub>"]. epoch_pub is the secp256k1 public key corresponding to the current epoch key.

All other tags are defined in Community.

Epoch Key System

All content in a private group is encrypted using an epoch key. The epoch key is a 32-byte value held only by members in their clients — never by any relay or server.

Epoch Key as a secp256k1 Keypair

The epoch key is a valid secp256k1 private key scalar. Its public counterpart is derived normally:

epoch_pub = epoch_key × G

epoch_pub is published openly in the kind:10444 and kind:30444 events. epoch_key is a secret shared among members. Any member who holds epoch_key can distribute it to new members — there is no special admin role required. Key management tooling (bunkers, remote signers, etc.) can be used to handle this securely.

Encryption

All event content fields are encrypted using NIP-44 with the epoch key encrypting to its own public key:

conversation_key = HKDF(ECDH(epoch_key, epoch_pub), 'nip44-v2')

Because ECDH(epoch_key, epoch_pub) = epoch_key × epoch_pub = epoch_key² × G, this produces a fixed, deterministic conversation_key for the epoch. Any holder of epoch_key can encrypt and decrypt. The relay sees only the ciphertext.

Members verify they hold the correct epoch key by checking epoch_key × G == announced epoch_pub before publishing.

Key Derivation (Ratchet)

Epoch keys form a hash chain:

epoch_key_{n+1} = HKDF-Expand(epoch_key_n, "group-epoch-advance", 32)

Any member who holds epoch_key_n can derive all subsequent epoch keys without any network round-trip. Clients SHOULD derive and cache epoch_key_{n+1} before the scheduled advance, and MUST delete epoch_key_n once the epoch advances to achieve forward secrecy.

Chain Breaks

When a member is removed, any remaining member with the current epoch key generates a new random epoch_key_R (breaking the derivation chain), distributes it to remaining members via key delivery events, and publishes a new kind:30444 epoch announcement with epoch_pub_R. The removed member retains all epoch keys up to epoch_key_{R-1} but cannot derive epoch_key_R or any subsequent key.

Epoch Advance Schedule

Scheduled advances provide forward secrecy without requiring any member to be online. Any member who holds epoch_key_0 pre-signs a series of kind:30444 epoch announcement events (e.g., one per week for a year) using the group keypair and uploads them all to the relay at once. Because kind:30444 is parameterized replaceable, each epoch number is a separate d tag and all coexist on the relay simultaneously. Members derive future epoch keys locally from their stored root — no DMs, no round-trips.

Emergency chain breaks (on member removal or suspected key compromise) interrupt the pre-signed schedule. A member generates a new random root, publishes a new kind:30444 with a d tag that supersedes the current epoch, distributes the new root to remaining members, and updates the kind:10444 event to reference the new epoch.

Epoch Announcement Event (kind:30444)

The group key publishes one kind:30444 event per epoch. Each event is individually addressable as <30444:<group-pubkey>:<epoch-number>. This allows the full schedule to be pre-signed and uploaded in a single session.

{
  "id": "<event-id>",
  "pubkey": "<group-pubkey>",
  "kind": 30444,
  "created_at": 1675642635,
  "tags": [
    ["d", "7"],                               // epoch number — makes each epoch uniquely addressable
    ["h", "<group-pubkey>"],
    ["epoch-pub", "<epoch_7_pub>"],           // public half of epoch_7_key
    ["next-epoch-pub", "<epoch_8_pub>"],      // pre-announced so clients can derive and cache epoch_8_key
    ["advance-at", "<unix-timestamp>"]        // scheduled time this epoch becomes active
  ],
  "content": "",
  "sig": "<group-sig>"
}

Clients fetch kind:30444 events from the group pubkey on join. The current epoch is the highest-numbered event whose advance-at is in the past. The next epoch can be pre-derived and cached from next-epoch-pub before its advance-at arrives.

To prepare a year of automatic advances, any member pre-signs 52 kind:30444 events (one per week) using the group keypair and uploads them all. The relay stores all 52; clients determine which is current from the advance-at timestamps.

Key Delivery Event (kind:444)

When a member joins or a chain break occurs, any existing member delivers the current epoch key via a kind:444 event encrypted directly to the new member. The sender signs with their own key; the group key does not need to be online.

{
  "id": "<event-id>",
  "pubkey": "<sender-pubkey>",
  "kind": 444,
  "created_at": 1675642635,
  "tags": [
    ["p", "<recipient-pubkey>"],
    ["h", "<group-pubkey>"]
  ],
  "content": "<NIP-44 encrypted JSON, see below>",
  "sig": "<sender-sig>"
}

The encrypted content MUST be a JSON object:

{
  "epoch_key": "<hex>",
  "epoch_num": 7,
  "epoch_pub": "<hex>",
  "group": "<group-pubkey>"
}

Encrypted with NIP-44 using conversation_key = HKDF(ECDH(sender_priv, recipient_pub), 'nip44-v2').

The recipient MUST verify epoch_key × G == epoch_pub before accepting the key.

Any member can deliver: Any existing member who holds the current epoch key MAY send a kind:444. The new member accepts the first valid delivery they receive that matches the current epoch_pub in the kind:10444 event.

Multi-device: Since Nostr users share one keypair across devices, a kind:444 DM is readable on any device holding the user’s private key. Clients SHOULD store received epoch keys in local encrypted storage so the DM does not need to be re-fetched.

Publishing Encrypted Content

Content events are identical in structure to their public counterparts with two changes:

  1. The content field is replaced with a NIP-44 ciphertext using the current epoch conversation key
  2. An ["epoch", "<epoch-number>"] tag is added

All other tags — h, e, a, p, d — remain plaintext. The event is signed by the author’s own key.

{
  "id": "<event-id>",
  "pubkey": "<author-pubkey>",
  "kind": 9,
  "created_at": 1675642635,
  "tags": [
    ["h", "<group-pubkey>"],
    ["epoch", "7"]
  ],
  "content": "<NIP-44 ciphertext>",
  "sig": "<author-sig>"
}

The same pattern applies to all other content kinds (11, 1111, 7, 30315, 30316, 32267, 30063, etc.).

Interactions

Comments (kind:1111) and reactions (kind:7) reference the event they interact with via e and a tags. These reference tags remain plaintext — the event ID of the target event is public. Only the content (reaction string or comment text) is encrypted. This is an acceptable metadata tradeoff: it is visible that someone reacted to a given event, but not what they said or which reaction.

Replaceable and Addressable Events

Addressable events (kinds with a d tag, such as kind:30315 projects and kind:30063 releases) work cleanly: a single ciphertext is published and updated in place under the same d tag. No fan-out is required. Members decrypt the latest version using the epoch key current at the time of publication; if the epoch has since advanced, the event carries its epoch number and the client derives the correct key from its stored root.

Blossom Media

Binary media files SHOULD be AES-256-GCM encrypted before uploading to blossom. This requires no changes to the blossom server — it stores the encrypted blob by its SHA256 hash as normal. The file decryption key travels inside the NIP-44 encrypted event content, so only group members can retrieve it.

For each file:

  1. Generate a random 32-byte file_key
  2. Encrypt the file with AES-256-GCM using file_key
  3. Upload the encrypted blob to blossom; it is addressed by SHA256(encrypted_blob)
  4. In the event’s encrypted content, include the blossom URL and file_key alongside any other file metadata
{
  "url": "https://blossom.example/abc123...def.bin",
  "file_key": "<hex>",
  "mime_type": "image/jpeg",
  "size": 204800
}

Members decrypt the event, extract file_key, fetch the blob, and decrypt. The blossom server sees only an opaque encrypted blob and its hash — no file contents, no keys.

Relay Enforcement

The relay MUST NOT hold any epoch key or group private key at any time.

A dedicated group relay SHOULD:

  • Require AUTH for reads and writes to prevent metadata leakage
  • Reject events from pubkeys not present in the relevant section’s profile list
  • Serve kind:30444 epoch announcements to authenticated members

Any relay MUST store all pre-signed kind:30444 events regardless of their advance-at timestamp; clients determine which epoch is current.

Running on a public relay without AUTH is permitted. Content remains encrypted regardless; the tradeoff is that anyone can observe who is publishing to the group and when.

Member Management

Member management uses the same profile lists (kind:30000) as public communities. Adding a member: add their p tag to the relevant list and send them a kind:444 key delivery event. Removing a member: remove their p tag, then trigger an immediate chain break — generate new epoch_key_R, publish a new kind:30444 with a higher epoch number, update kind:10444 to reference the new epoch, and distribute epoch_key_R to all remaining members via kind:444.

The relay begins rejecting the removed member’s events as soon as their p tag is gone from the profile list. The chain break ensures they cannot decrypt future content even if they retain old epoch keys.

Requesting from the Relay

Request events with the group’s h tag from the relays listed in the group’s kind:10444 event. If the relay requires AUTH, complete the handshake first.

Clients MUST:

  1. Fetch kind:30444 events from the group pubkey to determine the current epoch and verify their stored epoch_key against the announced epoch-pub
  2. Check if a newer epoch key is needed (if joining mid-chain or after a chain break) and request a kind:444 from any existing member
  3. Decrypt received events using the epoch key corresponding to the event’s ["epoch", "N"] tag

Implementation Notes

Key storage: Clients MUST store epoch keys in local encrypted storage. Keys are not re-derivable from the user’s Nostr key. A client that loses its epoch key storage must request a new kind:444 from any existing member.

Forward secrecy: Clients MUST delete epoch_key_n once the epoch has advanced and all messages from epoch n have been locally cached. Retaining old epoch keys defeats forward secrecy.

Pre-signing: Any member with the group keypair SHOULD pre-sign kind:30444 epoch announcements for at least 3 months of scheduled advances during the initial setup session, and upload them all at once. Each is individually addressable by its d tag (epoch number), so all coexist on the relay. This allows the group to operate without the group keypair being online.

Chain break on suspected compromise: If a member’s key is suspected compromised, trigger an immediate chain break even if no explicit removal is happening. Generate a new random root, publish a new kind:30444 superseding the current epoch, update kind:10444, and redistribute to all remaining members via kind:444.

Metadata visible to the relay: The relay sees author pubkeys, timestamps, event IDs, and reference tags (e, a, p). It does not see content. The threat model is unauthorized content access, not traffic analysis.

Solo Use

A private group with a single member is a practical encrypted personal store. Compared to simply encrypting events to your own Nostr pubkey, the epoch key approach has a meaningful advantage: your Nostr key and your content encryption keys are entirely separate. If your Nostr key is ever compromised, an attacker gets your identity but not your past content — provided you have deleted old epoch keys as the epochs advanced. With direct self-encryption using your Nostr key, key compromise decrypts everything at once.

A solo private group also gives you the full content section structure — private notes, drafts, personal project tracking, encrypted media — and the ability to seamlessly invite others later without changing the encryption scheme.

Benefits

  1. Content is end-to-end encrypted; the relay operator cannot read it
  2. All events are signed by their real author — attribution is preserved
  3. Epoch-based key rotation provides forward secrecy without per-message overhead
  4. Scheduled advances require no member presence after initial setup
  5. Chain breaks on removal prevent excluded members from reading future content
  6. No special client architecture required — standard NIP-44 and secp256k1 primitives
  7. Any existing member can onboard new members or perform chain breaks — no privileged role
  8. Works as a single-user encrypted personal store with the option to invite others later
  9. Blossom media is fully encrypted end-to-end — the server stores only opaque blobs
  10. All content sections (chat, forum, projects, apps) work with the same encryption model
  11. Replaceable events update in place — no fan-out