This document explains the overall architecture of nostr-java and how its modules collaborate to implement the Nostr protocol.
Purpose: Provide a high-level mental model for contributors and integrators. Audience: Developers extending or integrating the library. Last Updated: 2026-02-24 (v2.0.0 simplification)
- Design Philosophy
- Module Overview
- Data Flow
- Event Lifecycle
- Core Classes
- Design Patterns
- Error Handling
- Security
nostr-java 2.0 follows a minimalist, protocol-aligned design:
- One event class —
GenericEventhandles every Nostr event kind viaint kind. No subclasses. - One tag class —
GenericTagstores a code andList<String>params. No subclasses, noElementAttribute. - Integer kinds —
Kindsutility provides named constants (Kinds.TEXT_NOTE,Kinds.CONTACT_LIST) and range checks. Any integer is valid — no enum gating. - 4 modules — each with a clear, focused purpose and a strict dependency chain.
- Virtual Threads — relay I/O and listener dispatch use Java 21 Virtual Threads for lightweight concurrency.
This design reduced the library from ~180 classes across 9 modules to ~40 classes across 4 modules, eliminating all NIP-specific concrete types, entity DTOs, factory hierarchies, and annotation-driven tag registration.
nostr-java-core → nostr-java-event → nostr-java-identity → nostr-java-client
Purpose: Foundation utilities and cryptographic primitives.
Key classes:
NostrUtil— SHA-256 hashing, hex encoding viajava.util.HexFormat, random byte generationSchnorr— BIP-340 Schnorr signature signing and verificationBech32/Bech32Prefix— Bech32 encoding/decoding (NIP-19)Nip05Validator— DNS-based identity validation with async/batch supportDefaultHttpClientProvider— HTTP client with shared Virtual Thread executor- Exception hierarchy:
NostrException,NostrCryptoException,NostrEncodingException,NostrNetworkException
Dependencies: None (foundation layer). External: BouncyCastle, commons-lang3, Jackson.
Purpose: Event model, tag model, filters, messages, and JSON serialization.
Key classes:
GenericEvent— The sole event class. Supports any kind viaint. ImplementsISignable.GenericTag— The sole tag class.code+List<String> params. Factory:GenericTag.of("e", "eventId", "relay").Kinds— Staticintconstants for common kinds plus range-check methods (isReplaceable(),isEphemeral(),isAddressable()).EventFilter— Builder-based composable filter for relay REQ messages.Filters— Container for multipleEventFilterinstances (OR logic).PublicKey,PrivateKey,Signature— Value objects with Bech32 encoding.ISignable— Signing contract implemented byGenericEvent.BaseMessageand subclasses —EventMessage,ReqMessage,CloseMessage,OkMessage,EoseMessage,NoticeMessage.- Serialization:
GenericEventSerializer,GenericEventDeserializer,GenericTagSerializer,TagDeserializer,EventSerializer(canonical NIP-01),EventJsonMapper.
Dependencies: nostr-java-core.
Purpose: Identity management, signing, and message encryption.
Key classes:
Identity— Key pair management, event signing viaidentity.sign(event).MessageCipher04— NIP-04 encrypted direct messages (legacy).MessageCipher44— NIP-44 versioned encryption (recommended).
Dependencies: nostr-java-event.
Purpose: WebSocket relay communication with retry, Virtual Threads, and async support.
Key classes:
NostrRelayClient— SpringTextWebSocketHandler-based WebSocket client. Blockingsend(), non-blockingsubscribe(), asyncconnectAsync()/sendAsync()/subscribeAsync().RelayTimeoutException— Typed exception for relay timeouts (replaces silent empty-list returns).ConnectionState— Enum:CONNECTING,CONNECTED,RECONNECTING,CLOSED.NostrRetryable/RetryConfig— Spring Retry annotation and configuration.
Dependencies: nostr-java-identity, Spring WebSocket, Spring Retry.
Application
│
▼
GenericEvent.builder() ← build event with kind, content, tags
│
▼
Identity.sign(event) ← compute ID (SHA-256) + Schnorr signature
│
▼
NostrRelayClient.send( ← send over WebSocket
new EventMessage(event))
│
▼
Relay ← receives ["EVENT", {...}]
- The application builds a
GenericEventusing the builder or constructor. Identity.sign()computes the canonical NIP-01 JSON, hashes it (SHA-256), and signs with BIP-340 Schnorr.- The signed event is wrapped in an
EventMessageand sent to relays viaNostrRelayClient. - Relay responses (OK, EOSE, NOTICE, EVENT) are parsed and returned or dispatched to listeners.
Build → Sign → Send → Receive response
// 1. Build
GenericEvent event = GenericEvent.builder()
.pubKey(identity.getPublicKey())
.kind(Kinds.TEXT_NOTE)
.content("Hello Nostr!")
.tags(List.of(GenericTag.of("t", "nostr")))
.build();
// 2. Sign
identity.sign(event);
// 3. Send
try (NostrRelayClient client = new NostrRelayClient("wss://relay.example.com")) {
List<String> responses = client.send(new EventMessage(event));
}The sole event class. All Nostr events are represented as GenericEvent regardless of kind.
@Data
public class GenericEvent implements ISignable {
private String id;
private PublicKey pubKey;
private Long createdAt;
private int kind;
private List<GenericTag> tags;
private String content;
private Signature signature;
}Key methods:
GenericEvent.builder()— fluent builderisReplaceable(),isEphemeral(),isAddressable()— kind range checkstoBech32()— NIP-19 encoding
The sole tag class. A tag is a code and a list of string parameters — exactly what the Nostr protocol specifies.
GenericTag.of("e", "eventId123", "wss://relay.example.com", "reply")
// Serializes to: ["e", "eventId123", "wss://relay.example.com", "reply"]
tag.getCode() // "e"
tag.getParams() // ["eventId123", "wss://relay.example.com", "reply"]
tag.toArray() // ["e", "eventId123", "wss://relay.example.com", "reply"]Static int constants for common event kinds. Users can use any integer — these are convenience constants, not a gating mechanism.
Kinds.TEXT_NOTE // 1
Kinds.SET_METADATA // 0
Kinds.CONTACT_LIST // 3
Kinds.ENCRYPTED_DIRECT_MESSAGE // 4
Kinds.isReplaceable(10002) // true (10000-19999)
Kinds.isEphemeral(20001) // true (20000-29999)
Kinds.isAddressable(30023) // true (30000-39999)Builder-based filter for relay subscriptions:
EventFilter filter = EventFilter.builder()
.kinds(List.of(Kinds.TEXT_NOTE, Kinds.REACTION))
.authors(List.of("pubkey_hex"))
.since(timestamp)
.until(timestamp)
.addTagFilter("t", List.of("nostr"))
.limit(100)
.build();GenericEvent.builder() and EventFilter.builder() provide fluent, readable construction of complex objects.
PublicKey, PrivateKey, Signature, SubscriptionId — immutable objects that compare by value, encapsulate validation, and provide Bech32 encoding.
GenericEvent delegates to EventValidator for validation, EventSerializer for canonical serialization, and Kinds for kind range classification.
GenericTag.of(code, params...) provides a concise way to create tags.
NostrRuntimeException (base)
├── NostrProtocolException (NIP violations)
├── NostrCryptoException (signing, encryption)
│ ├── SigningException
│ └── SchnorrException
├── NostrEncodingException (serialization)
│ ├── KeyEncodingException
│ ├── EventEncodingException
│ └── Bech32EncodingException
└── NostrNetworkException (relay communication)
└── RelayTimeoutException (timeout waiting for relay)
- Validate early — constructors and builders validate input
- Fail fast —
HexFormat.parseHex()throws on invalid hex;RelayTimeoutExceptionreplaces silent empty returns - Use domain exceptions — specific exceptions with context, not generic
RuntimeException
- Private keys never leave the process — signing is in-memory only
- Uses
SecureRandomwith BouncyCastle for key generation - Never reuse nonces or IVs
- BIP-340 Schnorr signatures on secp256k1
- Deterministic (RFC 6979) — same message produces same signature
- Verifiable by public key
- NIP-04 (legacy) — AES-256-CBC. Use NIP-44 for new applications.
- NIP-44 (recommended) — HKDF key derivation, ChaCha20-Poly1305 AEAD.
nostr-java 2.0 provides:
- Minimal API surface — one event class, one tag class, ~40 total classes across 4 modules
- Protocol-aligned design — kinds are integers, tags are string arrays, no library-imposed type hierarchy
- Virtual Thread concurrency — relay I/O and listener dispatch on lightweight threads
- Reliable connectivity — typed timeout exceptions, connection state tracking, Spring Retry
- Strong cryptography — BIP-340 Schnorr, NIP-44 AEAD, BouncyCastle provider