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). Pass 0 to 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 the relay tag matches this URL. Comparison normalizes scheme and host case, default ports (80 for ws, 443 for wss), 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 a nonce tag 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 with Accept: 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 (plain ws:// listener).
ssl_key_file - Optional path to a PEM private key file used with ssl_cert_file. Default: undef. If set, ssl_cert_file is 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_events is 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 custom store is 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 with count tokens, one token is consumed per event, and all tokens are refilled when seconds have elapsed since the last refill. When no tokens remain, events are rejected with an OK false response and a rate-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 the content field in an event. Events exceeding this limit are rejected with OK false and an invalid: prefix. Must be a positive integer. Default: undef (unlimited).
my $relay = Net::Nostr::Relay->new(max_content_length => 8196);
max_event_tags - Maximum number of tags allowed on an event. Events exceeding this limit are rejected with OK false and an invalid: prefix. Must be a positive integer. Default: undef (unlimited).
my $relay = Net::Nostr::Relay->new(max_event_tags => 2000);
max_limit - Server-side cap on the limit field 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 - Default limit applied 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 with created_at older than now - created_at_lower_limit are rejected with OK false. Matches the NIP-11 limitation field 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 with created_at more than this many seconds ahead of the current time are rejected with OK false. Matches the NIP-11 limitation field 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 $accepted is false, the event is rejected with OK false and 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 when graceful_stop is 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