Skip to content

Custom Relay Handler Development

The @contextvm/sdk’s-pluggable architecture, centered around the RelayHandler interface, allows developers to implement custom logic for managing Nostr-relay connections. This is particularly useful for advanced use cases that require more sophisticated behavior than what the default SimpleRelayPool provides.

You might want to create a custom RelayHandler for several reasons:

  • Intelligent Relay Selection: To dynamically select relays based on performance, reliability, or the specific type of data being requested. For example, you might use a different set of relays for fetching user metadata versus broadcasting messages.
  • Auth Relays: To integrate with auth relays that require authentication or specific connection logic.
  • Dynamic Relay Discovery: To discover and connect to new relays at runtime, rather than using a static list.
  • Custom Caching: To implement a custom caching layer to reduce redundant requests to relays.
  • Resiliency and-failover: To build more robust-failover logic, such as automatically retrying failed connections or switching to backup relays.

A critical requirement for implementing the RelayHandler interface is that the subscribe method must be non-blocking. This design ensures that the transport layer can create multiple subscriptions concurrently without waiting for each one to complete.

  1. Immediate Return: The subscribe method should return immediately after initiating the subscription
  2. Internal State Management: Store active subscriptions internally for lifecycle management
  3. Automatic Reconnection: Handle resubscription when connections are reestablished

To create a custom relay handler, you need to create a class that implements the RelayHandler interface. This involves implementing five methods: connect, disconnect, publish, subscribe, and unsubscribe.

Implementation Pattern For Non-Blocking Subscriptions

Section titled “Implementation Pattern For Non-Blocking Subscriptions”
class MyRelayHandler implements RelayHandler {
private subscriptions: Array<{
filters: Filter[];
onEvent: (event: NostrEvent) => void;
onEose?: () => void;
closer?: SubCloser; // Or similar subscription management object
}> = [];
async connect(): Promise<void> {
// Connect to the relays
}
async disconnect(relayUrls?: string[]): Promise<void> {
// Disconnect from the relays
}
async publish(event: NostrEvent): Promise<void> {
// Publish the event to the relays
}
async subscribe(
filters: Filter[],
onEvent: (event: NostrEvent) => void,
onEose?: () => void,
): Promise<void> {
// Create the subscription (non-blocking)
const closer = this.pool.subscribeMany(relayUrls, filters, {
onevent: onEvent,
oneose: onEose,
});
// Store the subscription for management
this.subscriptions.push({ filters, onEvent, onEose, closer });
}
unsubscribe(): void {
// Close all active subscriptions
this.subscriptions.forEach((sub) => sub.closer?.close());
this.subscriptions = [];
}
}

This pattern is used by both SimpleRelayPool and ApplesauceRelayPool implementations.

Once your custom handler class is created, you can instantiate it and pass it to any component that requires a RelayHandler, such as the NostrClientTransport or NostrServerTransport. The SDK will then use your custom logic for all relay interactions.

With the Relay component covered, we will now look at the high-level bridging components provided by the SDK.