A framework for building super custom relays you can rely on. Designed to be simple and stable. About 1500 lines of code.
go get github.com/pippellia-btc/rely
Getting started is easy, and deep customization is just as straightforward.
relay := NewRelay()
if err := relay.StartAndServe(ctx, "localhost:3334"); err != nil {
panic(err)
}
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)
)
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
}
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
)
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.
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.
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"