Events and Messages in Nostr
Learning Objectives
After this lesson, you'll understand:
- How everything in Nostr is structured as events
- Different types of events and their purposes
- Event structure and required fields
- How events are signed and verified
- Common event kinds and their use cases
Understanding Events
In Nostr, everything is an event. Whether you're posting a text note, updating your profile, reacting to a post, or sending a direct message - it's all structured as events.
This unified approach provides several benefits:
- Consistency: All data follows the same structure
- Extensibility: New features can be added as new event types
- Simplicity: One format to rule them all
- Interoperability: All clients understand the same basic structure
Event Structure
Every Nostr event is a JSON object with specific required fields:
{
"id": "event-id-hash",
"pubkey": "author-public-key",
"created_at": 1234567890,
"kind": 1,
"tags": [],
"content": "Hello Nostr!",
"sig": "cryptographic-signature"
}
Let's break down each field:
Required Fields
id
(Event ID)
- 32-byte SHA-256 hash of the serialized event data
- Serves as the unique identifier for the event
- Calculated from other fields (not arbitrary)
// The ID is the SHA-256 of this serialized data:
[
0, // Reserved
pubkey, // Author public key
created_at, // Timestamp
kind, // Event type
tags, // Tags array
content // Event content
]
pubkey
(Author)
- 32-byte public key of the event creator
- Identifies who created the event
- Used to verify the signature
created_at
(Timestamp)
- Unix timestamp in seconds
- When the event was created
- Used for chronological ordering
kind
(Event Type)
- Integer that defines the event type
- Determines how clients should interpret the event
- Standardized in various NIPs
tags
(Metadata)
- Array of arrays containing metadata
- Used for references, mentions, hashtags, etc.
- Each tag is an array of strings
content
(Message Content)
- The main content of the event
- Can be text, JSON, or empty depending on kind
- Often contains the user-visible message
sig
(Signature)
- 64-byte Schnorr signature
- Proves the event was created by the pubkey owner
- Prevents tampering
Event Kinds
Event kinds determine how the event should be interpreted:
Text Events
Kind 1: Text Note
The most common event type - like a tweet:
{
"kind": 1,
"content": "Just learned about Nostr! This decentralized social media is amazing π",
"tags": [
["t", "nostr"],
["t", "decentralized"]
]
}
Kind 0: User Metadata
Profile information:
{
"kind": 0,
"content": "{\"name\":\"Alice\",\"about\":\"Nostr enthusiast\",\"picture\":\"https://example.com/avatar.jpg\"}"
}
Social Events
Kind 7: Reaction
Likes, hearts, and other reactions:
{
"kind": 7,
"content": "π€",
"tags": [
["e", "note-id-being-reacted-to"],
["p", "author-of-original-note"]
]
}
Kind 6: Repost
Sharing someone else's note:
{
"kind": 6,
"content": "",
"tags": [
["e", "event-id-being-reposted"],
["p", "original-author-pubkey"]
]
}
Communication Events
Kind 4: Encrypted Direct Message
Private messages between users:
Kind 42: Channel Message
Public chat room messages:
{
"kind": 42,
"content": "Hello everyone in this channel!",
"tags": [
["e", "channel-creation-event-id", "", "root"]
]
}
Management Events
Kind 3: Contact List
Who you follow:
{
"kind": 3,
"content": "",
"tags": [
["p", "pubkey1", "relay-url", "petname"],
["p", "pubkey2", "relay-url", "alice"]
]
}
Kind 5: Event Deletion
Request to delete your own events:
Advanced Events
Kind 30023: Long-form Content
Articles and blog posts:
{
"kind": 30023,
"content": "# My Article\n\nThis is a long-form article...",
"tags": [
["d", "my-article-slug"],
["title", "My Amazing Article"],
["summary", "A brief summary"],
["published_at", "1234567890"]
]
}
Kind 9735: Zap
Lightning payments:
{
"kind": 9735,
"content": "",
"tags": [
["bolt11", "lightning-invoice"],
["description", "zap-request-event"],
["p", "recipient-pubkey"]
]
}
Tag System
Tags provide structured metadata for events:
Common Tag Types
"e" tags - Event References
Reference other events:
event-id
: The event being referencedrelay-url
: Where to find the event (optional)marker
: "root", "reply", or "mention" (optional)pubkey
: Author of referenced event (optional)
"p" tags - Pubkey References
Reference users:
"t" tags - Topics/Hashtags
Categorize content:
"d" tags - Identifiers
For replaceable events:
Advanced Tags
// Content warning
["content-warning", "reason"]
// Expiration
["expiration", "unix-timestamp"]
// Subject line
["subject", "Email-like subject"]
// Geographic location
["g", "geohash"]
Event Creation Process
1. Build Event Object
const event = {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "nostr"],
["p", "some-pubkey"]
],
content: "Hello Nostr!"
}
2. Add Pubkey
3. Calculate ID
4. Sign Event
5. Publish to Relays
Event Verification
When receiving events, clients must verify them:
1. Verify ID
import { getEventHash } from 'nostr-tools'
const calculatedId = getEventHash(event)
if (calculatedId !== event.id) {
throw new Error('Invalid event ID')
}
2. Verify Signature
import { verifySignature } from 'nostr-tools'
const isValid = verifySignature(event)
if (!isValid) {
throw new Error('Invalid signature')
}
3. Check Timestamp
const now = Math.floor(Date.now() / 1000)
const age = now - event.created_at
// Reject events too far in the future
if (event.created_at > now + 60) {
throw new Error('Event from future')
}
// Optionally reject very old events
if (age > 86400 * 30) { // 30 days
console.warn('Very old event')
}
Event Serialization
Events must be serialized consistently for ID calculation:
// Serialization for ID calculation
const serialized = JSON.stringify([
0,
event.pubkey,
event.created_at,
event.kind,
event.tags,
event.content
])
// No whitespace, specific character escaping
const id = sha256(utf8Encode(serialized))
Character Escaping Rules
Specific characters must be escaped in content:
- Line break (
0x0A
) β\n
- Double quote (
0x22
) β\"
- Backslash (
0x5C
) β\\
- Carriage return (
0x0D
) β\r
- Tab (
0x09
) β\t
- Backspace (
0x08
) β\b
- Form feed (
0x0C
) β\f
Event Relationships
Events can reference each other to create complex structures:
Threads (Replies)
{
"kind": 1,
"content": "This is a reply",
"tags": [
["e", "root-event-id", "", "root"],
["e", "parent-event-id", "", "reply"],
["p", "original-author-pubkey"],
["p", "parent-author-pubkey"]
]
}
Mentions
Quotes
{
"kind": 1,
"content": "This is interesting: nostr:note1abc...",
"tags": [
["q", "quoted-event-id"]
]
}
Replaceable Events
Some events can be replaced by newer versions:
Regular Replaceable (10000-19999)
Only the latest event for each kind
+ pubkey
is kept:
{
"kind": 10000,
"content": "My mute list",
"tags": [
["p", "muted-pubkey-1"],
["p", "muted-pubkey-2"]
]
}
Parameterized Replaceable (30000-39999)
Latest event for each kind
+ pubkey
+ d
tag:
{
"kind": 30023,
"tags": [
["d", "my-article-slug"],
["title", "My Article"]
],
"content": "Article content..."
}
Event Validation Rules
Required Validations
id
matches SHA-256 of serialized eventsig
is valid Schnorr signaturepubkey
is valid 32-byte hexcreated_at
is reasonable timestampkind
is valid integertags
is array of arrays of strings
Optional Validations
- Content length limits
- Tag count limits
- Timestamp freshness
- Proof of work requirements
- Content filtering
Working with Events in Code
Creating a Text Note
import { finishEvent } from 'nostr-tools'
const event = finishEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
["t", "hello"],
["t", "nostr"]
],
content: "Hello Nostr world! π"
}, privateKey)
console.log('Created event:', event)
Creating a Reply
const replyEvent = finishEvent({
kind: 1,
created_at: Math.floor(Date.now() / 1000),
tags: [
["e", originalEvent.id, "", "root"],
["p", originalEvent.pubkey]
],
content: "Great post! Thanks for sharing."
}, privateKey)
Creating a Reaction
const reaction = finishEvent({
kind: 7,
created_at: Math.floor(Date.now() / 1000),
tags: [
["e", noteEvent.id],
["p", noteEvent.pubkey],
["k", "1"] // kind of event being reacted to
],
content: "π€"
}, privateKey)
Best Practices
For Event Creation
Do This
- Always set reasonable
created_at
timestamps - Include relevant
p
tags for notifications - Use standard tag formats
- Keep content size reasonable
- Include proper event references in replies
Avoid This
- Creating events with future timestamps
- Omitting required
p
tags in replies - Using non-standard tag formats
- Creating excessive tag noise
- Forgetting to handle special characters
For Event Processing
- Always verify signatures before trusting events
- Implement reasonable timestamp checks
- Handle missing or malformed fields gracefully
- Cache verification results for performance
- Rate limit event processing to prevent spam
Common Patterns
Thread Creation
// Root post
const rootPost = finishEvent({
kind: 1,
content: "Starting a new thread about Nostr events...",
// ...
}, privateKey)
// Reply to root
const reply = finishEvent({
kind: 1,
content: "First point: events are the core data structure",
tags: [
["e", rootPost.id, "", "root"],
["p", rootPost.pubkey]
]
// ...
}, privateKey)
Content Discovery
// Subscribe to hashtag
const sub = relay.sub([{
kinds: [1],
"#t": ["nostr"]
}])
// Subscribe to mentions
const mentionSub = relay.sub([{
kinds: [1],
"#p": [myPubkey]
}])
Event Updates
// Replaceable event (profile)
const profile = finishEvent({
kind: 0,
content: JSON.stringify({
name: "Alice",
about: "Nostr developer",
picture: "https://example.com/avatar.jpg"
})
// ...
}, privateKey)
Next Steps
Understanding events is crucial for building Nostr applications. Next, explore:
- Relays and Communication - How events travel through the network
- NIPs - Specifications for different event types
- Building Applications - Putting it all together
Practice Exercise
Try creating different types of events using the examples above. Start with simple text notes and gradually explore more complex event types like replies and reactions.