| title | description | category | tags | difficulty | last-updated | |||
|---|---|---|---|---|---|---|---|---|
NIP Protocol Reference |
Definitive reference for all Nostr Implementation Proposal (NIP) implementations in the platform. |
reference |
|
beginner |
2026-01-16 |
Definitive reference for all Nostr Implementation Proposal (NIP) implementations in the platform.
Version: 1.0.0 Status: Production
This document consolidates all Nostr Improvement Proposal (NIP) implementations. It serves as the definitive reference for protocol specifications, event structures, and implementation details.
- NIP-01: Basic Protocol
- NIP-04: Encrypted DMs (Legacy)
- NIP-06: Key Derivation
- NIP-09: Event Deletion
- NIP-10: Text Notes & Replies
- NIP-17: Private DMs
- NIP-25: Reactions
- NIP-28: Public Chat (Deprecated)
- NIP-29: Relay-Based Groups
- NIP-42: Authentication
- NIP-44: Encrypted Payloads
- NIP-50: Search Capability
- NIP-52: Calendar Events
- NIP-59: Gift Wrap
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/01.md
Foundation of all Nostr events. Defines event structure, signing, and verification.
interface NostrEvent {
id: string; // SHA-256 hash of serialised event
pubkey: string; // Hex-encoded public key
created_at: number; // Unix timestamp (seconds)
kind: number; // Event type identifier
tags: string[][]; // Array of tag arrays
content: string; // Event content (arbitrary string)
sig: string; // Schnorr signature
}| Kind | Name | Description | Implementation |
|---|---|---|---|
| 0 | Metadata | User profile (name, about, picture) | ✅ createUserMetadata() |
| 1 | Text Note | Short-form text post | ✅ createTextNote() |
| 3 | Contacts | Contact list | ❌ Not used |
| 4 | Encrypted DM | Legacy encrypted messages | 🟡 Read-only (deprecated) |
| 5 | Deletion | Event deletion request | ✅ createDeletionEvent() |
| 7 | Reaction | Event reaction (like, emoji) | ✅ createReaction() |
| 9 | Group Message | NIP-29 channel message | ✅ Primary chat mechanism |
// Serialisation format for ID hash
const serialised = JSON.stringify([
0, // Reserved for future use
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
]);
// Hash with SHA-256
const id = sha256(serialised);// Verify steps:
// 1. Compute event ID from serialised content
const computedId = sha256(serialiseEvent(event));
// 2. Verify ID matches event.id
if (computedId !== event.id) return false;
// 3. Verify schnorr signature
return schnorr.verify(event.sig, event.id, event.pubkey);Implementation: verifyEventSignature() in /src/lib/nostr/events.ts
Tags are arrays where:
- First element: tag name (single letter or string)
- Subsequent elements: tag values
- Last element (optional): relay hint
// Event reference tag
['e', eventId, relayUrl?, marker?]
// Pubkey reference tag
['p', pubkey, relayUrl?, petName?]
// Generic tag
['customTag', 'value1', 'value2']| Tag | Name | Purpose | Example |
|---|---|---|---|
| e | Event | Reference another event | ['e', '123abc...', '', 'root'] |
| p | Pubkey | Reference a user | ['p', '456def...'] |
| t | Topic | Hashtag/topic | ['t', 'nostr'] |
| r | Reference | URL reference | ['r', 'https://example.com'] |
| h | Group | NIP-29 group identifier | ['h', 'channel-id'] |
Status: 🟡 Read-Only (Deprecated) Official Spec: https://github.com/nostr-protocol/nips/blob/master/04.md
NIP-04 is deprecated in favour of NIP-17 (Private DMs) + NIP-59 (Gift Wrap). The platform:
- Reads NIP-04 messages for backwards compatibility
- Never creates new NIP-04 messages
- Migrates existing conversations to NIP-17 when possible
{
kind: 4,
content: encryptedContent, // AES-256-CBC encrypted
tags: [
['p', recipientPubkey] // Single recipient
]
}- Metadata Leakage: Recipient visible to relay
- Weak Encryption: CBC mode vulnerable to padding oracle attacks
- No Forward Secrecy: Compromised key exposes all messages
- No Sender Hiding: Sender pubkey exposed
Migration Path: Use NIP-17 for all new DMs.
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/06.md
Deterministic key generation from BIP39 mnemonic phrases.
m / 44' / 1237' / account' / change / index
Standard Path: m/44'/1237'/0'/0/0
44'- BIP44 purpose1237'- Nostr coin type0'- Account 0 (hardened)0- External chain0- Key index
import { generateMnemonic, mnemonicToSeed } from 'bip39';
import { HDKey } from '@scure/bip32';
// Generate 24-word mnemonic
const mnemonic = generateMnemonic(256);
// Derive seed
const seed = await mnemonicToSeed(mnemonic);
// Derive key at standard path
const hdkey = HDKey.fromMasterSeed(seed);
const path = "m/44'/1237'/0'/0/0";
const derived = hdkey.derive(path);
const privateKey = derived.privateKey; // 32 bytes
const publicKey = derived.publicKey; // 33 bytes (compressed)Security:
- Mnemonic must be stored securely (encrypted)
- Never transmit mnemonic over network
- Validate mnemonic checksum before use
Related: Key storage in /src/lib/utils/storage.ts
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/09.md
User-requested deletion of their own events.
{
kind: 5,
content: reason, // Optional deletion reason
tags: [
['e', eventId1],
['e', eventId2],
// ... multiple events can be deleted
]
}- Authorisation: Only event author can request deletion
- Relay Discretion: Relays MAY honour deletion (not guaranteed)
- Propagation: Deletion events should be broadcast to all relays
- UI Handling: Clients SHOULD hide deleted events
// Create deletion event
const deletionEvent = createDeletionEvent(
[eventId1, eventId2],
privateKey,
'Accidental post' // Optional reason
);
// Publish to relay
await ndk.publish(deletionEvent);Code: createDeletionEvent() in /src/lib/nostr/events.ts
For channel messages, admins can delete any message using:
- Kind 9005 (NIP-29 admin deletion)
- Different from NIP-09 (user can only delete own)
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/10.md
Threading model for text notes using 'e' tags with positional markers.
// Root event (top of thread)
['e', rootEventId, relayUrl, 'root']
// Direct reply target
['e', replyEventId, relayUrl, 'reply']
// Additional mentions (context)
['e', mentionEventId, relayUrl, 'mention']Post A (root)
├─ Reply B ['e', A, '', 'root']
│ └─ Reply C ['e', A, '', 'root'], ['e', B, '', 'reply']
└─ Reply D ['e', A, '', 'root']
// Create root post
const rootPost = createTextNote(
'This is a root post',
privateKey
);
// Create reply
const reply = createTextNote(
'This is a reply',
privateKey,
rootPost.id // replyTo parameter
);
// Tags added automatically:
// ['e', rootPost.id, '', 'root']
// ['e', rootPost.id, '', 'reply']- Always include root tag in nested replies
- Use reply marker for direct parent
- Limit mention tags to avoid spam
- Include 'p' tags for mentioned users
Code: Threading logic in /src/lib/nostr/events.ts
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/17.md
Private direct messages with sealed rumour structure. Replaces NIP-04.
// Inner event (kind 14) - NOT signed or published directly
{
kind: 14,
content: encryptedContent, // NIP-44 encrypted
created_at: actualTimestamp,
tags: [
['p', recipientPubkey]
]
// No id, sig, or pubkey fields
}The sealed rumour is wrapped in a gift wrap event (kind 1059):
{
kind: 1059,
pubkey: randomPubkey, // Random, not sender's
created_at: fuzzedTime, // ±2 days from actual
content: encryptedRumor, // Sealed rumour encrypted
tags: [
['p', recipientPubkey]
]
}- Sender Hiding: Random pubkey in wrapper
- Timestamp Fuzzing: ±2 days from real time
- Relay Blinding: Relay cannot identify sender
- Forward Secrecy: Each message uses unique encryption
// 1. Create sealed rumour
const rumor = {
kind: 14,
content: await encryptNIP44(plaintext, recipientPubkey),
created_at: Date.now() / 1000,
tags: [['p', recipientPubkey]]
};
// 2. Generate random wrapper key
const randomKey = generateRandomKey();
// 3. Create gift wrap
const wrapper = {
kind: 1059,
pubkey: getPublicKey(randomKey),
created_at: fuzzTimestamp(Date.now() / 1000),
content: await encryptNIP44(JSON.stringify(rumor), recipientPubkey),
tags: [['p', recipientPubkey]]
};
// 4. Sign and publish wrapper
await signAndPublish(wrapper, randomKey);Code: sendDM() in /src/lib/nostr/dm.ts
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/25.md
Emoji reactions to events (likes, custom emojis).
{
kind: 7,
content: reaction, // '+' for like, or emoji
tags: [
['e', targetEventId],
['p', targetAuthorPubkey]
]
}| Content | Meaning |
|---|---|
+ |
Like / upvote |
- |
Dislike / downvote |
❤️ |
Heart |
😂 |
Laugh |
| Custom emoji | Any Unicode emoji |
// React with like
const reaction = createReaction(
eventId,
authorPubkey,
privateKey,
'+' // or any emoji
);
await ndk.publish(reaction);// Count reactions by content
const reactions = await fetchReactions(eventId);
const counts = reactions.reduce((acc, r) => {
acc[r.content] = (acc[r.content] || 0) + 1;
return acc;
}, {});
// Result: { '+': 42, '❤️': 15, '😂': 8 }Code: Reactions in /src/lib/stores/reactions.ts
Status: 🟡 Deprecated (Use NIP-29) Official Spec: https://github.com/nostr-protocol/nips/blob/master/28.md
NIP-28 is deprecated. Use NIP-29 for all channel operations.
NIP-28 had limitations:
- No admin controls
- No member management
- No private channels
- Limited moderation
| Kind | Name | Description |
|---|---|---|
| 40 | Channel Create | Create public channel |
| 41 | Channel Metadata | Update channel info |
| 42 | Channel Message | Send message |
| 43 | Hide Message | Hide message in client |
| 44 | Mute User | Mute user in channel |
Migration: All NIP-28 channels converted to NIP-29.
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/29.md
Relay-based group chat with admin controls, membership management, and moderation.
| Kind | Name | Description | Access |
|---|---|---|---|
| 9 | Group Message | Channel message | Members |
| 11 | Group Thread | Threaded reply | Members |
| 12 | Group Reply | Quick reply | Members |
| 39000 | Group Metadata | Channel info | Admins |
| 39001 | Group Admins | Admin list | Admins |
| 39002 | Group Members | Member list | Admins |
| 9000 | Add User | Invite to group | Admins |
| 9001 | Remove User | Kick from group | Admins |
| 9002 | Edit Metadata | Update info | Admins |
| 9005 | Delete Message | Remove message | Admins |
| 9006 | Create Invite | Generate invite | Admins |
| 9007 | Join Request | Request membership | Anyone |
{
kind: 9,
content: messageText,
tags: [
['h', groupId], // REQUIRED: group identifier
['e', replyToId, '', 'reply'], // Optional: threading
['p', mentionedPubkey] // Optional: mentions
]
}Key Difference from NIP-28:
- Uses 'h' tag for group ID (not 'e')
- Kind 9 instead of kind 42
{
kind: 39000,
content: '', // Empty or JSON
tags: [
['d', groupId], // Replaceable event identifier
['name', 'Group Name'],
['about', 'Description'],
['picture', 'https://...'],
['public'], // or ['private']
]
}{
kind: 9007,
content: 'Please add me',
tags: [
['h', groupId]
]
}Relay Processing:
- Relay receives join request
- Notifies group admins
- Admin approves with kind 9000 (Add User)
- Relay adds user to member list
- User can now post kind 9 messages
{
kind: 9000,
tags: [
['h', groupId],
['p', userPubkey]
]
}{
kind: 9001,
tags: [
['h', groupId],
['p', userPubkey]
]
}{
kind: 9005,
tags: [
['h', groupId],
['e', messageEventId]
]
}NIP-29 relays MUST:
- Verify membership before accepting kind 9 messages
- Enforce admin permissions for moderation actions
- Maintain member lists (kind 39002)
- Notify admins of join requests
Code Locations:
- Channel operations:
/src/lib/nostr/groups.ts - Admin actions:
/src/lib/stores/admin.ts - Join requests:
/src/routes/admin/+page.svelte
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/42.md
Relay authentication using cryptographic challenge-response.
sequenceDiagram
participant Client
participant Relay
Client->>Relay: Subscribe/Publish
Relay->>Client: AUTH challenge
Note over Relay: ["AUTH", "<challenge>"]
Client->>Client: Sign challenge
Client->>Relay: AUTH response (kind 22242)
Relay->>Relay: Verify signature
alt Signature valid
Relay->>Client: OK
else Signature invalid
Relay->>Client: NOTICE "Auth failed"
end
{
kind: 22242,
content: '',
tags: [
['relay', relayUrl],
['challenge', challengeString]
],
created_at: timestamp,
// ... standard event fields
}// Listen for AUTH challenges
ndk.pool.on('auth', async (relay, challenge) => {
// Create AUTH event
const authEvent = new NDKEvent(ndk);
authEvent.kind = 22242;
authEvent.tags = [
['relay', relay.url],
['challenge', challenge]
];
// Sign and send
await authEvent.sign();
await relay.send(['AUTH', authEvent.rawEvent()]);
});Relay Usage:
- Whitelist enforcement: Only authenticated users can publish
- Private groups: Verify membership before message delivery
- Rate limiting: Per-user instead of per-IP
Code: RelayManager in /src/lib/nostr/relay.ts
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/44.md
Modern encryption standard for Nostr. Replaces NIP-04 with better security.
Version 2 (Current):
- Key Agreement: ECDH (secp256k1)
- KDF: HMAC-SHA256
- Cipher: ChaCha20-Poly1305
- Nonce: Random 12 bytes
- Authentication: Poly1305 MAC
// 1. Compute shared secret
const sharedSecret = ecdh(senderPrivkey, recipientPubkey);
// 2. Derive encryption key
const key = hmacSha256(sharedSecret, 'nip44-v2-key');
// 3. Generate random nonce
const nonce = randomBytes(12);
// 4. Encrypt with ChaCha20-Poly1305
const ciphertext = chacha20poly1305.encrypt(
plaintext,
key,
nonce
);
// 5. Format: version || nonce || ciphertext || tag
const payload = Buffer.concat([
Buffer.from([0x02]), // Version 2
nonce,
ciphertext,
authTag
]);
// 6. Base64 encode
const encrypted = payload.toString('base64');// 1. Base64 decode
const payload = Buffer.from(encrypted, 'base64');
// 2. Parse components
const version = payload[0]; // Must be 0x02
const nonce = payload.slice(1, 13);
const ciphertext = payload.slice(13, -16);
const authTag = payload.slice(-16);
// 3. Compute shared secret
const sharedSecret = ecdh(recipientPrivkey, senderPubkey);
// 4. Derive decryption key
const key = hmacSha256(sharedSecret, 'nip44-v2-key');
// 5. Decrypt with ChaCha20-Poly1305
const plaintext = chacha20poly1305.decrypt(
ciphertext,
authTag,
key,
nonce
);✅ Authenticated Encryption: Poly1305 MAC prevents tampering ✅ Random Nonces: Each message has unique nonce ✅ Forward Secrecy: Unique nonce provides session-like properties ✅ Malleability Protection: AEAD prevents ciphertext modification
- Private DMs (NIP-17): Encrypt DM content
- Private Channels: Encrypt group messages
- Gift Wrapping (NIP-59): Encrypt sealed rumours
Code: Encryption in /src/lib/nostr/encryption.ts
Status: ✅ Partially Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/50.md
Relay-side search capability with optional filters.
interface SearchFilter extends Filter {
search?: string; // Search query
}
// Example
const filter = {
kinds: [1, 9],
search: 'nostr protocol',
limit: 20
};Relays advertise NIP-50 support in NIP-11 document:
{
"name": "My Relay",
"supported_nips": [1, 9, 29, 50],
"search": {
"enabled": true,
"operators": ["AND", "OR", "NOT"],
"case_sensitive": false
}
}Client-Side:
- Semantic search using HNSW index
- Embedding-based similarity search
- Fallback to keyword search if relay doesn't support NIP-50
Relay-Side:
- Full-text search with PostgreSQL
ts_vector - Ranked results by relevance
- Support for boolean operators
Code: Search in /src/lib/semantic/hnsw-search.ts
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/52.md
Time-based calendar events and RSVPs.
{
kind: 31923,
content: '', // Optional description
tags: [
['d', uniqueId], // Replaceable event ID
['title', 'Event Title'],
['start', unixTimestamp.toString()],
['end', unixTimestamp.toString()], // Optional
['location', 'Place or URL'], // Optional
['h', groupId] // Optional: channel association
]
}{
kind: 31925,
content: '', // Optional comment
tags: [
['d', uniqueId], // Per-user replaceable
['a', '31923:pubkey:eventId'], // Event reference
['status', status] // 'accepted' | 'declined' | 'tentative'
]
}| Status | Meaning |
|---|---|
accepted |
Attending |
declined |
Not attending |
tentative |
Maybe attending |
// Create calendar event
const event = await createCalendarEvent({
title: 'Weekly Meeting',
description: 'Team sync',
start: Date.now() / 1000,
end: Date.now() / 1000 + 3600,
location: 'https://meet.example.com',
channelId: 'channel-id'
});
// RSVP to event
await rsvpToEvent(event.id, 'accepted');Features:
- Replaceable events (update by overwriting)
- Per-user RSVP tracking
- Channel association for group events
- Recurring events (via multiple event creation)
Code: Calendar in /src/lib/nostr/calendar.ts
Status: ✅ Fully Implemented Official Spec: https://github.com/nostr-protocol/nips/blob/master/59.md
Metadata hiding for private messages using sealed rumour wrapper.
{
kind: 1059,
pubkey: randomKey, // Random ephemeral key
created_at: fuzzedTime, // Actual time ± random offset
content: sealedRumor, // NIP-44 encrypted rumour
tags: [
['p', recipientPubkey]
],
// Standard id and sig fields
}// Fuzz timestamp by ±2 days
const fuzzTimestamp = (actualTime: number): number => {
const twoDays = 2 * 24 * 60 * 60;
const offset = (Math.random() * 2 - 1) * twoDays;
return Math.floor(actualTime + offset);
};- Sender Anonymity: Random pubkey hides real sender
- Timing Anonymity: Fuzzing hides message time
- Content Privacy: NIP-44 encryption
- Recipient Privacy: Only recipient can decrypt
Relay sees:
{
kind: 1059,
pubkey: '7f8e...', // Random (different each time)
created_at: 1704672123, // Fuzzed (±2 days)
content: 'AgE7HpA...', // Encrypted blob
tags: [['p', 'abc123...']] // Only recipient known
}Relay cannot determine:
- Who sent the message
- When it was actually sent
- What the content is
- If the recipient is the real target
Used automatically with NIP-17 DMs:
// Send DM (automatically gift-wrapped)
await sendDM(recipientPubkey, 'Secret message');
// Receive DM (automatically unwraps)
receiveDM(myPubkey, (dm) => {
console.log('From:', dm.sender); // Real sender
console.log('At:', dm.timestamp); // Real timestamp
console.log('Content:', dm.content); // Decrypted
});Code: Gift wrap in /src/lib/nostr/dm.ts
| NIP | Title | Version | Notes |
|---|---|---|---|
| 01 | Basic Protocol | 1.0 | Foundation |
| 06 | Key Derivation | 1.0 | BIP39 mnemonic |
| 09 | Event Deletion | 1.0 | User deletions |
| 10 | Text Notes | 1.0 | Threading |
| 17 | Private DMs | 1.0 | Sealed rumours |
| 25 | Reactions | 1.0 | Emoji reactions |
| 29 | Groups | 1.0 | Primary chat |
| 42 | Authentication | 1.0 | Relay AUTH |
| 44 | Encryption | 2.0 | ChaCha20-Poly1305 |
| 52 | Calendar | 1.0 | Events & RSVPs |
| 59 | Gift Wrap | 1.0 | Metadata hiding |
| NIP | Title | Status | Notes |
|---|---|---|---|
| 04 | Encrypted DMs (Legacy) | Read-only | Use NIP-17 instead |
| 11 | Relay Info | Supported | NIP-11 document |
| 28 | Public Chat | Deprecated | Use NIP-29 instead |
| 50 | Search | Client-side | Semantic search |
| NIP | Title | Priority | Use Case |
|---|---|---|---|
| 23 | Long-form | Low | Forum posts |
| 56 | Reporting | Low | Spam reports |
| 58 | Badges | Low | Achievements |
sequenceDiagram
participant User
participant Client
participant Relay
participant Members
User->>Client: Send message
Client->>Client: Create kind 9 event
Client->>Client: Add 'h' tag (group ID)
Client->>Client: Sign event
Client->>Relay: Publish event
Relay->>Relay: Verify signature
Relay->>Relay: Check membership
alt Member verified
Relay->>Relay: Store event
Relay->>Members: Broadcast to members
else Not a member
Relay->>Client: NOTICE "Not a member"
end
sequenceDiagram
participant Sender
participant Client
participant Relay
participant Recipient
Sender->>Client: Send DM
Client->>Client: Encrypt content (NIP-44)
Client->>Client: Create sealed rumour (kind 14)
Client->>Client: Generate random key
Client->>Client: Create gift wrap (kind 1059)
Client->>Client: Fuzz timestamp
Client->>Relay: Publish gift wrap
Note over Relay: Sees only:<br/>random pubkey<br/>fuzzed time<br/>encrypted blob
Relay->>Recipient: Deliver event
Recipient->>Recipient: Unwrap gift
Recipient->>Recipient: Decrypt content
Recipient->>Recipient: Extract real sender & time
✅ Always verify before publishing:
// Validate content
if (!validateContent(content).valid) {
throw new Error('Invalid content');
}
// Check rate limits
const rateLimit = checkRateLimit('message');
if (!rateLimit.allowed) {
throw new RateLimitError(rateLimit.retryAfter);
}
// Verify signature before sending
if (!verifyEventSignature(event)) {
throw new Error('Invalid signature');
}✅ Use correct markers:
// NIP-29 channel message
['h', channelId] // NOT ['e', channelId]
// NIP-10 threading
['e', rootId, '', 'root']
['e', parentId, '', 'reply']
// NIP-25 reactions
['e', targetEventId]
['p', targetAuthorPubkey]✅ Always use NIP-44 for new content:
// Modern (NIP-44)
const encrypted = await encryptNIP44(plaintext, recipientPubkey);
// Legacy (NIP-04) - only for reading old messages
const decrypted = await decryptNIP04Legacy(ciphertext, senderPubkey);✅ Handle AUTH challenges:
ndk.pool.on('auth', async (relay, challenge) => {
const authEvent = createAuthEvent(relay.url, challenge);
await authEvent.sign();
await relay.send(['AUTH', authEvent.rawEvent()]);
});Official NIP Repository:
Specifications: