rely

package module
v0.0.0-...-d39e982 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2025 License: MIT Imports: 26 Imported by: 0

README

rely

A framework for building super custom Nostr relays you can rely on. Written in Go, it's designed to be simple and performant, while providing an exeptional developer experience.

Go Reference Go Report Card

Installation

go get github.com/nostr-net/rely

Simple and Customizable

Getting started is easy, and deep customization is just as straightforward.

relay := NewRelay()
relay.StartAndServe(ctx, "localhost:3334")
Structural Customization

Fine-tune core parameters using functional options:

relay := rely.NewRelay(
    rely.WithDomain("myDomain.com"),	// required for NIP-42 validation
	rely.WithLogger(myLogger),			// configure the relay logger
	rely.WithInfo(myRelayInfo)			// set up nip-11 information document
)

To find all the available options and documentation, see options.go.

Behavioral Customization

You are not limited to simple configuration variables. The relay architecture facilitates complete behavioral customization by allowing you to inject your own functions into its Hooks. This gives you full control over the connection lifecycle, event flow and rate-limiting, enabling any custom business logic.

Below is a silly example that illustrates rely's flexibility.

func main() {
	// ...
	relay.Reject.Connection = append(relay.Reject.Connection, BadIP)
	relay.Reject.Event = append(relay.Reject.Event, RejectSatan)
	relay.On.Event = Save	// your custom DB save
}

func BadIP(s Stats, req *http.Request) error {
	if slices.Contains(blacklist, IP(req)) {
		return fmt.Errorf("you shall not pass!")
	}
	return nil
}

func RejectSatan(client Client, event *nostr.Event) error {
	if event.Kind == 666 {
		blacklist = append(blacklist, client.IP())
		client.Disconnect()
		return errors.New("not today, Satan. Not today")
	}
	return nil
}

You can find all the available hooks and documentation, in hooks.go.
If you need additional hooks, don't hesitate to open an issue!

If you are looking for inspiration or examples, check out the examples directory.

Intuitive interfaces

If you've read the NIP-01, you already know how to use rely.
Don't believe it? These three are the only interfaces that you'll have to deal with:

No obscure structures leaking implementation details, just intuitive methods that almost require no explanation. As an example, these are some of the methods of the Client interface (all safe for concurrent use).

type Client interface {
	IP() string
	
	Pubkey() string

	ConnectedAt() time.Time

	Subscriptions() []Subscription

	SendNotice(msg string)

	SendAuth()

	Disconnect()

	// All other methods...
	// (don't fear, there are be comments and docs for all methods)
}

Performant

Rely is completely written in Go, a memory-safe and low level language that is easy to learn and performant. It's particularly fit for building server-side applications thanks to its powerful concurrency primitives like channels and goroutines.

These features are extensively used in rely, creating a lock-free architecture on the hot-paths.
Inspired by strfry, rely also implements inverted indexes for matching broadcasted events with subscriptions, making this crucial operation very efficient. For more, check out the architecture section.

All of these optimizations allow a dummy implementation using rely to serve 8000+ concurrent "spammy" clients with less than 1GB of RAM on a 2017 i5 CPU (4-cores).

Secure by Design

To prevent resource abuse, each client is assigned a fixed-size queue for outgoing messages.
Before processing each REQ, the relay calculates the remaining free space and uses that number as a hard cap for the filters' limits. If the total request exceeds the budget, the larger filters are scaled down proportionally.

Here is an example illustrating it.

1. client's queue has a free capacity of 300.

2. client sends a REQ with two filters f1 and f2.
	f1: limit=10		f2: no limit

3. the filters' limits are modified to match the free capacity:
	f1: limit=10		f2: limit=290

This prevents waste of CPU and bandwidth on events the client will not see, and penalizes clients that request more than they consume. The size of the client's queue can be customized with the appropriate option.

Architecture

Client

The Client is the middleman between the websocket connection and the Relay. It reads and writes to the websocket in two separated goroutines, ensuring proper serialization.

Client.read:

  • reads from the websocket and parses nostr messages
  • applies the user defined Reject hooks
  • sends to the Processor's queue
  • handles NIP-42 authentication

Client.write:

  • receives responses in a dedicated queue
  • writes them to the websocket
Processor

The Processor received incoming requests (REQs, EVENTs, COUNTs), and handles them by applying the user defined On hooks (e.g. On.Event, On.Req, On.Count). It consumes from a dedicated queue with a limited but configurable number of worker goroutines.

Dispatcher

The Dispatcher is responsible for broadcasting newly received events to all matching subscriptions. To accomplish this task efficiently, it maintains inverted indexes for all active subscriptions of all clients, an approach inspired by strfry.

However, the Dispatcher is not the ultimate authority on the subscriptions state. Each client is the authority for its own subscriptions, while the dispatcher only maintains an eventually-consistent snapshot of the active ones. This decision is motivated by two reasons:

  1. having CLOSEs cancel the subscriptions as fast as possible to save work.
  2. making calls to Client.Subscriptions very efficiently.

Well tested

Rely fetures unit tests for components that make sense to test in isolation. More importantly, we have a random stress test where the relay is bombarded with thousands of connections, events, filters, and abrupt disconnections every second. This test alone allowed the discovery of hard concurrency bugs and race conditions impossible to detect with simplistic unit tests.

Used by

This section lists project and repositories that are using rely in production.

FAQs

Why does On.Req accept multiple filters?

As per NIP-01, a REQ can contain multiple filters. Returning only one filter (at the time) would prevent the user of rely from implementing custom query optimizations.

You can always loop through the filters and make a query for each.

When NIP-86?

Does anyone uses NIP-86? If so, please let me know.
Just so you know, you can embedd the Relay inside an http server, where you can configure all the methods you want.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidAuthRequest   = errors.New(`an AUTH request must follow this format: ['AUTH', {event_JSON}]`)
	ErrInvalidTimestamp     = errors.New(`created_at must be within one minute from the current time`)
	ErrInvalidAuthKind      = errors.New(`invalid AUTH kind`)
	ErrInvalidAuthChallenge = errors.New(`invalid AUTH challenge`)
	ErrInvalidAuthRelay     = errors.New(`invalid AUTH relay`)
)
View Source
var (
	ErrShuttingDown     = errors.New("the relay is shutting down, please try again later")
	ErrOverloaded       = errors.New("the relay is overloaded, please try again later")
	ErrUnsupportedNIP45 = errors.New("NIP-45 COUNT is not supported")
)
View Source
var (
	ErrGeneric         = errors.New(`the request must be a JSON array`)
	ErrUnsupportedType = errors.New(`the request type must be one between 'EVENT', 'REQ', 'CLOSE', 'COUNT' and 'AUTH'`)

	ErrInvalidEventRequest   = errors.New(`an EVENT request must follow this format: ['EVENT', {event_JSON}]`)
	ErrInvalidEventID        = errors.New(`invalid event ID`)
	ErrInvalidEventSignature = errors.New(`invalid event signature`)

	ErrInvalidReqRequest     = errors.New(`a REQ request must follow this format: ['REQ', {subscription_id}, {filter1}, {filter2}, ...]`)
	ErrInvalidCountRequest   = errors.New(`a COUNT request must follow this format: ['COUNT', {subscription_id}, {filter1}, {filter2}, ...]`)
	ErrInvalidSubscriptionID = errors.New(`invalid subscription ID`)
)

Functions

func ApplyBudget

func ApplyBudget(budget int, filters ...nostr.Filter)

ApplyBudget adjusts the Limit of each filter in-place so that the total does not exceed the given budget. Filters with limits <= budget / len(filters) are preserved, while larger ones are scaled down proportionally. It panics if budget is negative.

func DisconnectOnDrops

func DisconnectOnDrops(maxDropped int) func(c Client)

DisconnectOnDrops returns a When.GreedyClient function that sends a notice and disconnects the client if it dropped more than the maximum responses.

func HandleSignals

func HandleSignals(cancel context.CancelFunc)

HandleSignals listens to os signals, and then fires the cancel() function. This cancels the associated context, propagating the signal to the rest of the program.

func IP

func IP(r *http.Request) string

Extracts the IP address from the http request.

func InvalidID

func InvalidID(c Client, e *nostr.Event) error

InvalidID returns an error if the event's ID is invalid

func InvalidSignature

func InvalidSignature(c Client, e *nostr.Event) error

InvalidSignature returns an error if the event's signature is invalid.

func RegistrationFailWithin

func RegistrationFailWithin(d time.Duration) func(Stats, *http.Request) error

RegistrationFailWithin returns a Reject.Connection function that errs if a client registration has failed within the given duration.

Types

type Client

type Client interface {
	// UID is the unique identified for the client, useful to tie its identity to
	// external statistics or resources.
	UID() string

	// IP address of the client.
	IP() string

	// Pubkey the client used to authenticate with NIP-42, or an empty string if it didn't.
	// To initiate the authentication, call [Client.SendAuth].
	Pubkey() string

	// ConnectedAt returns the time when the client connected.
	ConnectedAt() time.Time

	// Age returns how long the client has been connected.
	// Short for time.Since(client.ConnectedAt()).
	Age() time.Duration

	// Subscriptions returns a snapshot of the currently active [Subscription]s of the client.
	Subscriptions() []Subscription

	// SendNotice to the client, useful for greetings, warnings and other informational messages.
	SendNotice(msg string)

	// SendAuth sends the client a newly generated AUTH challenge.
	// This resets the authentication state: any previously authenticated pubkey is cleared,
	// and a new challenge is generated and sent.
	SendAuth()

	// Disconnect the client, closing its websocket connection with a [websocket.CloseNormalClosure]
	Disconnect()

	// DroppedResponses returns the total number of responses that were dropped
	// because the client’s response channel was full. This value is monotonic
	// and it's useful for implementing backpressure or flow-control strategies.
	DroppedResponses() int

	// RemainingCapacity returns a snapshot of how many slots are currently
	// available in the client's response buffer. Useful for implementing
	// backpressure or flow-control strategies.
	RemainingCapacity() int
}

Client represents the nostr client connected to the relay. All methods are safe for concurrent use.

type Hooks

type Hooks struct {
	Reject RejectHooks
	On     OnHooks
	When   WhenHooks
}

Hooks provides a complete set of extension points allowing custom logic to be injected into the relay's lifecycle and operational flow.

These functions are categorized into three groups:

  • Reject: Preemptively blocks incoming data or connections before processing.
  • On: Handles standard lifecycle events (Connect, Disconnect, Auth) and successful data flows (Event, Req, Count).
  • When: Triggers on special, non-standard, or warning conditions.

All functions supplied must be thread-safe and must not be modified at runtime.

func DefaultHooks

func DefaultHooks() Hooks

type OnHooks

type OnHooks struct {
	// Connect runs immediately after a client has been connected and registered.
	// It is guaranteed to run before the Disconnect hook of the same client.
	// This callback must be very fast to avoid blocking the hot path.
	// For longer operations, use goroutines.
	//
	// Example:
	//   relay.On.Connect = func(c Client) {
	//       go longOperation(c)
	//   }
	Connect func(Client)

	// Disconnect runs immediately after a client has been unregistered and disconnected.
	// It is guaranteed to run after the Connect hook of the same client.
	// This callback must be very fast to avoid blocking the hot path.
	// For longer operations, use goroutines.
	//
	// Example:
	//   relay.On.Disconnect = func(c Client) {
	//       go longOperation(c)
	//   }
	Disconnect func(Client)

	// Auth is called immediately after a client successfully authenticates.
	// It can be used to load resources tied to the client’s public key or adjust rate limits.
	Auth func(Client)

	// Event defines how the relay processes an EVENT, for example by storing it in a database.
	Event func(Client, *nostr.Event) error

	// Req defines how the relay processes a REQ containing one or more filters,
	// for example by querying the database for matching events.
	// The provided context is canceled if the client sends the corresponding CLOSE message.
	Req func(context.Context, Client, nostr.Filters) ([]nostr.Event, error)

	// Count defines how the relay processes NIP-45 COUNT requests.
	// This hook is optional (= nil). If unset, COUNT requests are rejected with [ErrUnsupportedNIP45].
	Count func(Client, nostr.Filters) (count int64, approx bool, err error)
}

OnHooks defines functions invoked after specific relay events occur. These hooks customize how the relay reacts to client actions such as EVENT, REQ, and COUNT messages. Each function is called only after the corresponding input has passed all RejectHooks (if any).

OnHooks are typically used to implement custom processing, persistence, logging, authorization, or other side effects in response to relay activity.

func DefaultOnHooks

func DefaultOnHooks() OnHooks

type Option

type Option func(*Relay)

func WithClientResponseLimit

func WithClientResponseLimit(n int) Option

WithClientResponseLimit sets the maximum number of responses that can be buffered and sent to a single client connection before backpressure is applied. Must be greater than 0.

For each REQ, the framework dynamically adjusts the "limit" field across all filters to be less than the remaining capacity of the client's response channel:

sum filter's limit <= responseLimit - len(client.responses)

This ensures that the total number of events returned never exceeds what can be buffered and sent to the client, enforcing per-client backpressure and preventing overproduction of responses.

func WithDomain

func WithDomain(d string) Option

WithDomain sets the relay's official domain name (e.g., "example.com"). This is mandatory for validating NIP-42 authentication. If this is unset, NIP-42 authentication will fail, and a warning will be logged.

func WithInfo

func WithInfo(info nip11.RelayInformationDocument) Option

WithInfo sets a custom NIP-11 (Relay Information Document) JSON body returned when a request includes `Accept: application/nostr+json`. If not set, a default document is used.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the structured logger (*slog.Logger) used by the relay for all logging operations. If not set, a default logger will be used.

func WithMaxMessageSize

func WithMaxMessageSize(s int64) Option

WithMaxMessageSize sets the maximum size (in bytes) of a single incoming websocket message (e.g., a Nostr EVENT or REQ). Messages larger than this will be rejected. Must be > 512 bytes.

func WithMaxProcessors

func WithMaxProcessors(n int) Option

WithMaxProcessors sets the maximum number of concurrent workers (goroutines) used to process incoming client requests (EVENTs, REQs, COUNTs) from the internal queue. Must be greater than 0.

func WithPingPeriod

func WithPingPeriod(d time.Duration) Option

WithPingPeriod sets the interval at which the relay sends Ping messages to the client to keep the connection alive. Must be less than the pong wait and greater than 1s.

func WithPongWait

func WithPongWait(d time.Duration) Option

WithPongWait sets the read deadline for waiting for the next Pong message from the client after a Ping is sent. Must be greater than the ping period.

func WithQueueCapacity

func WithQueueCapacity(c int) Option

WithQueueCapacity sets the capacity of the internal channel used to queue incoming requests before they are processed by the worker pool. A larger capacity can help smooth out bursts of client activity.

func WithReadBufferSize

func WithReadBufferSize(s int) Option

WithReadBufferSize sets the read buffer size (in bytes) for the underlying websocket connection upgrader.

func WithWriteBufferSize

func WithWriteBufferSize(s int) Option

WithWriteBufferSize sets the write buffer size (in bytes) for the underlying websocket connection upgrader.

func WithWriteWait

func WithWriteWait(d time.Duration) Option

WithWriteWait sets the maximum duration to wait for a websocket write operation (including control messages) to complete before timing out and closing the connection. Must be greater than 1s.

type RejectHooks

type RejectHooks struct {
	// Connection is invoked before establishing a new client connection.
	// Returning a non-nil error rejects the connection.
	Connection []func(Stats, *http.Request) error

	// Event is invoked before processing an EVENT message.
	// Returning a non-nil error rejects the event.
	Event []func(Client, *nostr.Event) error

	// Req is invoked before processing a REQ message.
	// Returning a non-nil error rejects the request.
	Req []func(Client, nostr.Filters) error

	// Count is invoked before processing a NIP-45 COUNT request.
	// Returning a non-nil error rejects the request.
	Count []func(Client, nostr.Filters) error
}

RejectHooks defines optional functions that can preemptively reject certain actions before they are processed by the relay.

Each function in a hook slice is evaluated in order. If any function returns a non-nil error, the corresponding input (connection, event, request, or count) is immediately rejected.

These hooks are useful for enforcing access policies, validating input, or applying rate limits before the relay performs further processing.

func DefaultRejectHooks

func DefaultRejectHooks() RejectHooks

type Relay

type Relay struct {
	Hooks
	// contains filtered or unexported fields
}

Relay is the fundamental structure of the rely package, acting as an orchestrator for the other specialized actors in the system. Its main responsabilities are to register and unregister [clients], and route work to the specialized actors like [dispatcher] and [processor].

func NewRelay

func NewRelay(opts ...Option) *Relay

NewRelay creates a new Relay instance with sane defaults and customizable internal behavior. Customize its structure with functional options (e.g., WithDomain, WithQueueCapacity). Customize its behaviour by defining On.Event, On.Req and other Hooks.

Example:

relay := NewRelay(
    WithDomain("example.com"), // required for proper NIP-42 validation
    WithQueueCapacity(5000),
    WithPingPeriod(30 * time.Second),
)

func (*Relay) Broadcast

func (r *Relay) Broadcast(e *nostr.Event) error

Broadcast the event to all clients whose subscriptions match it.

func (*Relay) Clients

func (r *Relay) Clients() int

func (*Relay) Filters

func (r *Relay) Filters() int

func (*Relay) LastRegistrationFail

func (r *Relay) LastRegistrationFail() time.Time

func (*Relay) PrintStats

func (r *Relay) PrintStats()

Print important stats of the relay while it's running.

func (*Relay) QueueLoad

func (r *Relay) QueueLoad() float64

func (*Relay) ServeHTTP

func (r *Relay) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP implements the http.Handler interface, handling WebSocket connections and NIP-11 Relay Information Document requests.

func (*Relay) ServeNIP11

func (r *Relay) ServeNIP11(w http.ResponseWriter)

ServeNIP11 serves the NIP-11 relay information document.

func (*Relay) ServeWS

func (r *Relay) ServeWS(w http.ResponseWriter, req *http.Request)

ServeWS upgrades the http request to a websocket, creates a [client], and registers it with the Relay.

func (*Relay) Start

func (r *Relay) Start(ctx context.Context)

Start the relay in separate goroutines in a non-blocking fashion.

The relay will later need to be served using http.ListenAndServe or equivalent. See Relay.StartAndServe for an example on how to do it. For a proper shutdown process, you have to call Relay.Wait before closing your program.

func (*Relay) StartAndServe

func (r *Relay) StartAndServe(ctx context.Context, address string) error

StartAndServe starts the relay, listens to the provided address and handles http requests.

It's a blocking operation, that stops only when the context gets cancelled. Use Relay.Start if you don't want to listen and serve right away, but then don't forget to wait for a graceful shutdown with Relay.Wait.

func (*Relay) Subscriptions

func (r *Relay) Subscriptions() int

func (*Relay) TotalConnections

func (r *Relay) TotalConnections() int

func (*Relay) Wait

func (r *Relay) Wait()

Wait blocks until the relay has shut down completely.

This is useful only when you manually call Relay.Start instead of Relay.StartAndServe and need to wait for a graceful shutdown before the program exits.

type Stats

type Stats interface {
	// Clients returns the number of active clients connected to the relay.
	Clients() int

	// Subscriptions returns the number of active subscriptions.
	Subscriptions() int

	// Filters returns the number of active filters of REQ subscriptions.
	Filters() int

	// QueueLoad returns the ratio of queued requests to total capacity,
	// represented as a float between 0 and 1.
	QueueLoad() float64

	// LastRegistrationFail returns the last time a client failed to be added
	// to the registration queue, which happens during periods of high load.
	LastRegistrationFail() time.Time

	// TotalConnections returns the total number of connections since the relay startup.
	TotalConnections() int
}

Stats exposes relay statistics, useful for monitoring health or rejecting connections during peaks of activity. All methods are safe for concurrent use.

type Subscription

type Subscription interface {
	// UID is the unique subscription identifier that combines the [Client.UID]
	// with the user-provided subscription ID <Client.UID>:<subscription.ID>
	UID() string

	// ID is a unique identifier within the scope of its client.
	ID() string

	// Filters returns the filters of the subscription.
	Filters() nostr.Filters

	// Matches returns whether any of the subscription's filters match the provided event.
	Matches(*nostr.Event) bool

	// CreatedAt returns the time when the subscription was created.
	CreatedAt() time.Time

	// Age returns how long ago the subscription was created.
	// Short for time.Since(subscription.CreatedAt())
	Age() time.Duration

	// Close the subscription, and send the client a CLOSED message with the provided reason
	Close(reason string)
}

Subscription represent the nostr subscription created by a Client with a REQ. All methods are safe for concurrent use.

type WhenHooks

type WhenHooks struct {
	// GreedyClient is invoked when a client’s response buffer becomes full,
	// typically because it sends new REQs before reading responses from earlier ones.
	// This hook is commonly used for logging misbehavior or disconnecting the client.
	//
	// Warning:
	// Calling methods that lead to sending a response (e.g., [Client.SendAuth]
	// or [Client.SendNotice]) from within this hook is highly discouraged.
	//
	// If a send method is called here, and the response buffer remains full,
	// it will trigger this hook again, leading to uncontrolled recursion.
	//
	// To prevent crashes, any logic that calls a send method must include a
	// terminal condition (e.g., a counter or a flag) to guarantee the recursion will stop.
	GreedyClient func(Client)
}

WhenHooks defines functions invoked when special, non-standard, or exceptional conditions occur during the relay’s operation.

These hooks are useful for detecting and responding to client misbehavior or non-critical performance issues that fall outside the normal operational flow.

func DefaultWhenHooks

func DefaultWhenHooks() WhenHooks

Directories

Path Synopsis
cmd
nostr-relay command
examples
anti-crawlers command
auth command
basic command
blacklist command
clickhouse command
count command
dvm command
ip-rate command
logger command
nip11 command
sparing command
wot command
storage

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL