NAME
Net::Nostr::Relay - Nostr WebSocket relay server
SYNOPSIS
use Net::Nostr::Relay;
# Standalone relay (blocks until stop is called)
my $relay = Net::Nostr::Relay->new;
$relay->run('127.0.0.1', 8080);
# Non-blocking: run a relay and client together
use Net::Nostr::Key;
use Net::Nostr::Client;
my $relay = Net::Nostr::Relay->new;
$relay->start('127.0.0.1', 8080);
my $key = Net::Nostr::Key->new;
my $client = Net::Nostr::Client->new;
$client->connect('ws://127.0.0.1:8080');
my $event = $key->create_event(kind => 1, content => 'hello', tags => []);
$client->publish($event);
DESCRIPTION
An in-process Nostr relay. Accepts WebSocket connections, stores events using an indexed in-memory backend (or a pluggable custom store), manages subscriptions, and broadcasts new events to matching subscribers. Supports configurable event capacity with oldest-first eviction and per-connection rate limiting. Events do not persist across restarts unless a persistent storage backend is provided.
Implements:
NIP-01 - Basic protocol flow
NIP-09 - Event deletion requests
NIP-11 - Relay information document
NIP-13 - Proof of Work
NIP-40 - Expiration timestamp
NIP-42 - Authentication of clients to relays
NIP-45 - Event counts (HyperLogLog not supported)
NIP-70 - Protected events
NIP-77 - Negentropy syncing
Supports all NIP-01 event semantics:
Regular events - stored and broadcast
Replaceable events (kinds 0, 3, 10000-19999) - only latest per pubkey+kind
Ephemeral events (kinds 20000-29999) - broadcast but never stored
Addressable events (kinds 30000-39999) - only latest per pubkey+kind+d_tag
CONSTRUCTOR
new
my $relay = Net::Nostr::Relay->new;
my $relay = Net::Nostr::Relay->new(verify_signatures => 0);
my $relay = Net::Nostr::Relay->new(max_connections_per_ip => 10);
my $relay = Net::Nostr::Relay->new(relay_url => 'wss://relay.example.com/');
my $relay = Net::Nostr::Relay->new(relay_info => $info);
my $relay = Net::Nostr::Relay->new(
ssl_cert_file => 'cert.pem',
ssl_key_file => 'key.pem',
);
my $relay = Net::Nostr::Relay->new(min_pow_difficulty => 16);
my $relay = Net::Nostr::Relay->new(max_events => 10000);
my $relay = Net::Nostr::Relay->new(event_rate_limit => '10/60');
my $relay = Net::Nostr::Relay->new(store => $custom_store);
Creates a new relay instance. Options:
verify_signatures- Enable Schnorr signature verification (default: true). Pass0to disable (useful for testing with synthetic events).max_connections_per_ip- Maximum simultaneous WebSocket connections allowed from a single IP address. Connections beyond this limit are rejected at the TCP level. Default:undef(unlimited).relay_url- The relay's own WebSocket URL (e.g.wss://relay.example.com/). When set, NIP-42 AUTH events are validated to ensure therelaytag matches this URL. Comparison normalizes scheme and host case, default ports (80 forws, 443 forwss), and treats a missing path as/. Bracketed IPv6 addresses (e.g.ws://[::1]:8080) are supported. Default:undef(relay tag not validated).min_pow_difficulty- Minimum Proof of Work difficulty required for events (NIP-13). Events must have anoncetag committing to at least this difficulty, and the event ID must have at least this many leading zero bits. Events without a difficulty commitment are also rejected. Default:undef(no PoW required).-
my $relay = Net::Nostr::Relay->new(min_pow_difficulty => 16); relay_info- A Net::Nostr::RelayInfo object (NIP-11). When set, the relay serves the information document in response to HTTP requests withAccept: application/nostr+json, and handles CORS preflight OPTIONS requests. Default:undef(NIP-11 disabled).-
For plain listeners, the relay serves this document directly on the same port. TLS listeners accept secure WebSocket traffic natively, but do not perform the raw HTTP pre-read needed for direct NIP-11 document serving on that same socket. If you want HTTPS for the relay information document, terminate TLS in front of the relay or serve the document separately.
use Net::Nostr::RelayInfo; my $relay = Net::Nostr::Relay->new( relay_info => Net::Nostr::RelayInfo->new( name => 'My Relay', supported_nips => [1, 9, 11, 42], version => '1.0.0', ), ); ssl_cert_file- Path to a PEM certificate file used to accept secure WebSocket listeners (wss://). The PEM file may contain both the certificate and private key. Default:undef(plainws://listener).ssl_key_file- Optional path to a PEM private key file used withssl_cert_file. Default:undef. If set,ssl_cert_fileis required.-
my $relay = Net::Nostr::Relay->new( ssl_cert_file => 'cert.pem', ssl_key_file => 'key.pem', ); store- A pluggable storage backend object. Must implement the same interface as Net::Nostr::RelayStore (duck-typed). When provided,max_eventsis ignored (configure it on the store directly). Default: a new Net::Nostr::RelayStore instance.-
use Net::Nostr::RelayStore; my $store = Net::Nostr::RelayStore->new(max_events => 5000); my $relay = Net::Nostr::Relay->new(store => $store); max_events- Maximum number of events to retain in the default in-memory store. Oldest events are evicted when the limit is exceeded. Must be a positive integer. Default:undef(unlimited). Ignored when a customstoreis provided.-
my $relay = Net::Nostr::Relay->new(max_events => 10000); event_rate_limit- Per-connection event submission rate limit in the format"count/seconds"(e.g."10/60"for 10 events per 60 seconds). Uses a token bucket: each connection starts withcounttokens, one token is consumed per event, and all tokens are refilled whensecondshave elapsed since the last refill. When no tokens remain, events are rejected with anOK falseresponse and arate-limited:prefix. Default:undef(unlimited). Croaks if the format is invalid.-
my $relay = Net::Nostr::Relay->new(event_rate_limit => '10/60'); max_subscriptions- Maximum number of active subscriptions per connection. When a client sends a REQ that would exceed this limit, the relay responds with a CLOSED message. Replacing an existing subscription (same ID) does not count toward the limit. Must be a positive integer. Default:undef(unlimited).-
my $relay = Net::Nostr::Relay->new(max_subscriptions => 20); max_filters- Maximum number of filters allowed in a single REQ or COUNT message. Requests exceeding this limit are rejected with a CLOSED message. Must be a positive integer. Default:undef(unlimited).-
my $relay = Net::Nostr::Relay->new(max_filters => 10); max_content_length- Maximum length (in bytes) of thecontentfield in an event. Events exceeding this limit are rejected withOK falseand aninvalid:prefix. Must be a positive integer. Default:undef(unlimited).-
my $relay = Net::Nostr::Relay->new(max_content_length => 8196); -
my $relay = Net::Nostr::Relay->new(max_event_tags => 2000); max_limit- Server-side cap on thelimitfield in filters. If a client requests a higher limit (or no limit), it is silently capped to this value. Applies to both REQ and COUNT queries. Must be a positive integer. Default:undef(no cap).-
my $relay = Net::Nostr::Relay->new(max_limit => 500); default_limit- Defaultlimitapplied to filters that do not specify one. Without this, a filter with no limit returns all matching events. Must be a positive integer. Default:undef(no default limit).-
my $relay = Net::Nostr::Relay->new(default_limit => 100); created_at_lower_limit- Maximum age (in seconds) for events. Events withcreated_atolder thannow - created_at_lower_limitare rejected withOK false. Matches the NIP-11limitationfield of the same name. Must be a positive integer. Default:undef(no lower bound).-
# Reject events older than 1 year my $relay = Net::Nostr::Relay->new(created_at_lower_limit => 31536000); created_at_upper_limit- Maximum seconds into the future for events. Events withcreated_atmore than this many seconds ahead of the current time are rejected withOK false. Matches the NIP-11limitationfield of the same name. Must be a positive integer. Default:undef(no upper bound).-
# Reject events more than 15 minutes in the future my $relay = Net::Nostr::Relay->new(created_at_upper_limit => 900); max_message_length- Maximum incoming WebSocket message size in bytes. Messages exceeding this limit are dropped and a NOTICE is sent to the client. This is an application-level check; for frame-level protection, configure your reverse proxy. Must be a positive integer. Default:undef(unlimited).-
my $relay = Net::Nostr::Relay->new(max_message_length => 65536); on_event- A code reference called for each incoming event after structural validation but before storage and broadcast. Receives the Net::Nostr::Event object as its sole argument. Must return a two-element list($accepted, $message). If$acceptedis false, the event is rejected withOK falseand the given message (or a default if empty).-
my $relay = Net::Nostr::Relay->new( on_event => sub { my ($event) = @_; return (0, 'blocked: spam') if $event->content =~ /spam/; return (1, ''); }, ); idle_timeout- Seconds of client inactivity before the relay disconnects the connection. The timer resets each time the client sends a message. Must be a positive integer. Default:undef(no timeout).-
# Disconnect clients idle for more than 5 minutes my $relay = Net::Nostr::Relay->new(idle_timeout => 300); shutdown_timeout- Seconds to wait after sending a shutdown NOTICE before closing connections during "graceful_stop". Must be a positive integer. Default:undef(5 seconds whengraceful_stopis called).-
my $relay = Net::Nostr::Relay->new(shutdown_timeout => 10);Croaks on unknown arguments.
METHODS
run
$relay->run('127.0.0.1', 8080);
Starts the relay and blocks until stop is called. Equivalent to calling start followed by a blocking event loop.
start
$relay->start('127.0.0.1', 8080);
Starts listening for WebSocket connections on the given host and port. Returns immediately without blocking. Use this when you want to embed the relay in a larger application, run a client and relay in the same process, or compose with other AnyEvent watchers.
# Run a relay and client together
my $relay = Net::Nostr::Relay->new;
$relay->start('127.0.0.1', 8080);
my $client = Net::Nostr::Client->new;
$client->connect('ws://127.0.0.1:8080');
my $secure = Net::Nostr::Relay->new(
ssl_cert_file => 'cert.pem',
ssl_key_file => 'key.pem',
);
$secure->start('127.0.0.1', 8443);
my $secure_client = Net::Nostr::Client->new;
$secure_client->connect('wss://127.0.0.1:8443');
stop
$relay->stop;
Stops the relay, closes all connections, and clears all subscriptions. If the relay was started with run, also unblocks it. Safe to call on an unstarted relay.
graceful_stop
$relay->graceful_stop;
Initiates a graceful shutdown. Stops accepting new connections immediately, sends a NOTICE ("shutting down") to all connected clients, then closes all connections after shutdown_timeout seconds (default: 5). This gives clients time to reconnect to another relay.
my $relay = Net::Nostr::Relay->new(shutdown_timeout => 10);
$relay->start('0.0.0.0', 8080);
# ... later ...
$relay->graceful_stop; # NOTICE sent, connections close after 10s
broadcast
$relay->broadcast($event);
Sends the event to all connected clients whose subscriptions match. Normally called internally when a new event is accepted. Does not store the event -- use "inject_event" for storing without broadcasting, or publish via the normal EVENT protocol flow for both.
connections
my $conns = $relay->connections; # hashref (snapshot)
Returns a shallow copy of the active connections hash. Mutating the returned hashref does not affect the relay's internal state. Keys are connection IDs, values are AnyEvent::WebSocket::Connection objects.
subscriptions
my $subs = $relay->subscriptions; # hashref (snapshot)
Returns a two-level copy of the active subscriptions hash. Mutating the returned hashref (or its inner hashes) does not affect the relay's internal state. Keys are connection IDs, inner keys are subscription IDs, values are arrayrefs of Net::Nostr::Filter objects.
store
my $store = $relay->store;
Returns the storage backend object (Net::Nostr::RelayStore by default).
events
my $events = $relay->events; # arrayref of Net::Nostr::Event
Returns a snapshot (array copy) of stored events, sorted by created_at DESC then id ASC. Mutating the returned arrayref does not affect the store. Reflects replaceable/addressable semantics (only the latest version of each replaceable or addressable event is retained). Ephemeral events are never stored.
Can also be used as a setter for backward compatibility. The setter clears the store and re-stores each event individually (duplicates are silently skipped, and max_events eviction applies):
$relay->events([]); # clear all events
$relay->events([$event1, $event2]); # replace with given events
inject_event
my $ok = $relay->inject_event($event);
Stores an event directly into the store without validation or broadcasting. Returns 1 on success, 0 if the event is a duplicate. Useful for tests and programmatic seeding of relay state.
my $relay = Net::Nostr::Relay->new(verify_signatures => 0);
$relay->inject_event($event); # 1
$relay->inject_event($event); # 0 (duplicate)
max_events
my $max = $relay->max_events;
Returns the configured maximum event capacity, or undef if unlimited (the default). This value is passed to the default store on construction.
event_rate_limit
my $limit = $relay->event_rate_limit; # e.g. '10/60' or undef
Returns the per-connection event rate limit string, or undef if unlimited (the default). See "new" for token bucket semantics.
my $relay = Net::Nostr::Relay->new(event_rate_limit => '10/60');
verify_signatures
my $bool = $relay->verify_signatures;
Returns whether Schnorr signature verification is enabled (default: true).
max_connections_per_ip
my $limit = $relay->max_connections_per_ip;
Returns the maximum number of simultaneous connections allowed per IP address, or undef if unlimited (the default).
my $relay = Net::Nostr::Relay->new(max_connections_per_ip => 10);
$relay->start('0.0.0.0', 8080);
min_pow_difficulty
my $min = $relay->min_pow_difficulty;
Returns the minimum Proof of Work difficulty required for events (NIP-13), or undef if not set (the default).
my $relay = Net::Nostr::Relay->new(min_pow_difficulty => 16);
$relay->start('0.0.0.0', 8080);
relay_url
my $url = $relay->relay_url;
Returns the relay's own WebSocket URL, or undef if not set. Used for NIP-42 relay tag validation.
my $relay = Net::Nostr::Relay->new(relay_url => 'wss://relay.example.com/');
$relay->start('0.0.0.0', 8080);
relay_info
my $info = $relay->relay_info;
Returns the Net::Nostr::RelayInfo object (NIP-11), or undef if not set.
my $relay = Net::Nostr::Relay->new(
relay_info => Net::Nostr::RelayInfo->new(name => 'My Relay'),
);
$relay->start('0.0.0.0', 8080);
# Clients can now fetch: curl -H 'Accept: application/nostr+json' http://localhost:8080/
On TLS listeners, "relay_info" metadata can still describe the relay, but the built-in direct NIP-11 HTTP response path is only available on plain listeners.
max_subscriptions
my $max = $relay->max_subscriptions;
Returns the per-connection subscription limit, or undef if unlimited.
max_filters
my $max = $relay->max_filters;
Returns the per-REQ/COUNT filter count limit, or undef if unlimited.
max_content_length
my $max = $relay->max_content_length;
Returns the maximum event content length in bytes, or undef if unlimited.
max_event_tags
my $max = $relay->max_event_tags;
Returns the maximum event tag count, or undef if unlimited.
max_limit
my $max = $relay->max_limit;
Returns the server-side cap on filter limit, or undef if no cap.
default_limit
my $default = $relay->default_limit;
Returns the default limit applied to filters without one, or undef.
created_at_lower_limit
my $secs = $relay->created_at_lower_limit;
Returns the maximum event age in seconds, or undef if no bound.
created_at_upper_limit
my $secs = $relay->created_at_upper_limit;
Returns the maximum seconds-into-future for events, or undef if no bound.
max_message_length
my $max = $relay->max_message_length;
Returns the maximum incoming message size in bytes, or undef if unlimited.
on_event
my $cb = $relay->on_event;
Returns the event policy callback, or undef if not set.
idle_timeout
my $secs = $relay->idle_timeout;
Returns the idle timeout in seconds, or undef if not set.
shutdown_timeout
my $secs = $relay->shutdown_timeout;
Returns the graceful shutdown drain period in seconds, or undef if not set.
authenticated_pubkeys
my $auth = $relay->authenticated_pubkeys; # deep snapshot
Returns a deep copy of authenticated pubkeys per connection (NIP-42). Mutating the returned hashref does not affect the relay's internal state. Keys are connection IDs, values are hashrefs of pubkey hex strings.
my $auth = $relay->authenticated_pubkeys;
for my $conn_id (keys %$auth) {
for my $pubkey (keys %{$auth->{$conn_id}}) {
say "Connection $conn_id authenticated as $pubkey";
}
}
SEE ALSO
NIP-01, NIP-42, NIP-45, NIP-77, Net::Nostr, Net::Nostr::Client, Net::Nostr::Event, Net::Nostr::RelayStore, Net::Nostr::RelayInfo