Skip to content

A framework for building super custom nostr relays you can rely on. Designed to be simple and stable. About 1500 lines of code.

License

Notifications You must be signed in to change notification settings

pippellia-btc/rely

Repository files navigation

rely

A framework for building super custom relays you can rely on. Designed to be simple and stable. About 1500 lines of code.

Installation

go get github.com/pippellia-btc/rely

Simple and Customizable

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

relay := NewRelay()
if err := relay.StartAndServe(ctx, "localhost:3334"); err != nil {
	panic(err)
}

Structural Customization

Fine-tune core parameters using functional options:

relay := NewRelay(
    WithDomain("example.com"),      // required for proper NIP-42 validation
	WithQueueCapacity(10_000),   	// increase capacity to absorb traffic bursts (higher RAM)
	WithMaxProcessors(10),       	// increase concurrent processors for faster execution (higher CPU)
)

Behavioral Customization

Define behavior by simply writing RelayFunctions:

func main() {
	// ...
	relay.RejectConnection = append(relay.RejectConnection, BadIP)
	relay.RejectEvent = append(relay.RejectEvent, RejectSatan)
	relay.OnEvent = Save
}

func BadIP(s Stats, req *http.Request) error {
	if slices.Contains(blacklist, IP(req)) {
		return fmt.Errorf("you are not welcome here")
	}
	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
}

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.
This prevents waste of CPU and bandwidth on events that the client will not see, and penalizes clients that request more than they consume.

To configure the client’s queue capacity—and thus the maximum number of messages that can be sent at once—use:

relay := NewRelay(
    WithClientResponseLimit(100) // set max queue size to 100 messages per client
)

Why another framework

I started this new framework inspired by khatru but also frustrated by it. Despite its initial simplicity, achieving deep customization means dealing with (and understanding) the khatru relay structure. For the brave among you, here is the khatru relay struct, and for the even braver, here is the almighty HandleWebsocket method.

As a grug brain dev, I believe that complexity kills good software. Instead, I've built a simple architecture that doesn't introduce unnecessary abstractions: There is a relay, there are clients connecting to it, each with their own subscriptions. That's it.

Well tested

How do you test a relay framework?

You bombard a dummy implementation with thousands of connections, random events, random filters, random disconnects, and see what breaks. Then you fix it and repeat. If you find bugs please open a well-written issue and I'll fix it.

Here is a video showing rely handling up to 3500 concurrent clients, each sending one EVENT/REQ/s, all while handling 100 new http requests/s.

FAQs

Why does `relay.OnReq` accept multiple filters?

Because I don't want to hide the fact that a REQ can contain multiple filters, and I want the user of the framework to deal with it.
For example, he/she can decide to reject REQs that contain too many filters, or doing something like the following

func TooMany(client rely.Client, filters nostr.Filters) error {
    total := len(filters)
    for _, sub := range client.Subscriptions() {
        total += len(sub.Filters)
    }

    if total > 10 {
        client.Disconnect()
        return errors.New("rate-limited: too many open filters")
    }
    return nil
}
When [feature you like] ?

Open a well written issue and make a case for why it should be added. Keep in mind that, being a grug brain dev, I believe:

best weapon against complexity spirit demon is magic word: "no"

About

A framework for building super custom nostr relays you can rely on. Designed to be simple and stable. About 1500 lines of code.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages