Skip to main content

How Nostr Works

This page explains the fundamental architecture of Nostr and how clients, relays, and events work together.

Architecture Overview

Nostr follows a simple client-relay model:

Nostr Architecture

The Three Components

1. Clients

Clients are user-facing applications. They:

  • Generate or import user keypairs
  • Create and sign events
  • Connect to relays via WebSocket
  • Publish events to relays
  • Subscribe to events from relays
  • Render content for users

Clients can be web apps, mobile apps, desktop apps, CLI tools, or anything that speaks the Nostr protocol.

2. Relays

Relays are WebSocket servers that:

  • Accept connections from clients
  • Receive and validate events
  • Store events (persistence varies by relay)
  • Respond to subscription requests
  • Forward matching events to subscribers

Relays are "dumb"—they don't need to trust each other or coordinate. Each operates independently.

3. Events

Events are the atomic unit of data in Nostr. Every piece of information—posts, follows, reactions, profiles—is an event.

{
"id": "event identifier (sha256 hash)",
"pubkey": "author's public key",
"created_at": 1234567890,
"kind": 1,
"tags": [
["e", "referenced-event-id"],
["p", "referenced-pubkey"]
],
"content": "The message content",
"sig": "signature of the event"
}

Message Flow

Publishing an Event

Client                              Relay
│ │
│ 1. Create event JSON │
│ 2. Sign with private key │
│ │
│ ──["EVENT", {...}]──────────────►│
│ │
│ 3. Verify │
│ 4. Store │
│ │
│ ◄──["OK", id, true, ""]──────────│
│ │

Subscribing to Events

Client                              Relay
│ │
│ ──["REQ", "sub-id", {...}]──────►│
│ │
│ ◄──["EVENT", "sub-id", {...}]────│ (existing events)
│ ◄──["EVENT", "sub-id", {...}]────│
│ ◄──["EOSE", "sub-id"]────────────│ (end of stored events)
│ │
│ ◄──["EVENT", "sub-id", {...}]────│ (new events as they arrive)
│ │

WebSocket Messages

Nostr uses JSON arrays over WebSocket. The first element identifies the message type.

Client to Relay

MessageFormatDescription
EVENT["EVENT", <event>]Publish an event
REQ["REQ", <sub-id>, <filter>, ...]Subscribe to events
CLOSE["CLOSE", <sub-id>]Close a subscription

Relay to Client

MessageFormatDescription
EVENT["EVENT", <sub-id>, <event>]An event matching subscription
OK["OK", <event-id>, <success>, <msg>]Result of EVENT
EOSE["EOSE", <sub-id>]End of stored events
NOTICE["NOTICE", <message>]Human-readable message

Filters

Clients request events using filters:

{
"ids": ["specific-event-ids"],
"authors": ["pubkeys-to-follow"],
"kinds": [1, 6, 7],
"#e": ["event-ids-in-tags"],
"#p": ["pubkeys-in-tags"],
"since": 1234567890,
"until": 1234567899,
"limit": 100
}

Multiple Relays

Users typically connect to multiple relays for:

  • Redundancy: If one relay goes down, others still work
  • Reach: Different relays may have different users
  • Privacy: Spread your metadata across relays
const relays = [
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.nostr.band',
];

// Connect to all and publish to all
for (const url of relays) {
const ws = new WebSocket(url);
// ...
}

Event Verification

Events are self-authenticating through cryptographic signatures:

  1. Serialize the event content
  2. SHA256 hash produces the event ID
  3. Sign the ID with the private key
  4. Anyone can verify using the public key

This means relays don't need to trust clients, and clients don't need to trust relays.

Next Steps

Now that you understand the architecture: