| name | welshman-router |
| description | Use this skill when working with @welshman/router: relay selection, routing strategies, scenario-based relay routing, or choosing which relays to use for reads/writes. |
welshman/router — Relay Selection
@welshman/router provides scenario-based relay selection for nostr clients. It answers the question "which relays should I use for this operation?" by scoring candidate relays based on pubkey relay lists, relay quality, and configurable fallback policies. It sits between @welshman/util (types/helpers) and @welshman/net (actual relay connections), and is wrapped by @welshman/app for full-stack usage.
Installation
npm install @welshman/router
pnpm add @welshman/router
yarn add @welshman/router
Peer dependencies: @welshman/lib, @welshman/net, @welshman/util.
Key Exports
Router (class)
The main entry point. Use as a singleton via Router.configure() + Router.get(), or instantiate directly with options.
| Method | Description |
|---|
Router.configure(options) | Merge options into the global routerContext |
Router.get() | Return a Router instance using the global context |
new Router(options) | Create a router that overrides specific options from the global context |
RouterOptions (all optional):
| Option | Signature | Description |
|---|
getUserPubkey | () => string | undefined | Current user's pubkey |
getPubkeyRelays | (pubkey, mode?) => string[] | Relays for a pubkey; mode is "read", "write", or "messaging" |
getDefaultRelays | () => string[] | Fallback relays of last resort |
getIndexerRelays | () => string[] | Relays that index profiles and relay lists (NIP-65) |
getSearchRelays | () => string[] | Relays supporting NIP-50 search |
getRelayQuality | (url) => number | Quality score 0–1 for a relay (affects selection ranking) |
getLimit | () => number | Max relays returned by getUrls() (default: 3) |
Default behavior: if getPubkeyRelays is not configured, the router falls back to querying the local Repository (from @welshman/net) for kind-10002 events.
Router Scenario Methods
All return a RouterScenario. Naming convention: For* = relays to write to (so others can read), From* = relays to read from (author's outbox).
| Method | Description |
|---|
FromRelays(relays) | Use an explicit list of relay URLs |
ForUser() | User's read relays (where others can send things to the user) |
FromUser() | User's write relays (user's outbox) |
MessagesForUser() | User's messaging relays (NIP-17 DMs) |
ForPubkey(pubkey) | A pubkey's read relays |
FromPubkey(pubkey) | A pubkey's write relays (outbox) |
MessagesForPubkey(pubkey) | A pubkey's messaging relays |
ForPubkeys(pubkeys) | Merged read relays for multiple pubkeys |
FromPubkeys(pubkeys) | Merged write relays for multiple pubkeys |
MessagesForPubkeys(pubkeys) | Merged messaging relays for multiple pubkeys |
Event(event) | Event author's write relays (where the event lives) |
Replies(event) | Event author's read relays (where replies should be sent) |
PublishEvent(event) | Author's outbox + mentioned pubkeys' read relays; hard-limits to 30 |
Quote(event, id, hints) | Best relays to find a quoted event; checks tag relay hints and author pubkey from tag |
EventParents(event) | Relays for fetching parent events (from ancestor tags + mentioned pubkeys) |
EventRoots(event) | Relays for fetching root events |
Search() | Search relays |
Index() | Indexer relays |
Default() | Default/fallback relays |
merge(scenarios) | Combine multiple scenarios into one |
RouterScenario (class)
Immutable builder — every builder method returns a new instance. Terminal methods (getUrls(), getUrl()) return relay URLs, not instances.
| Method | Description |
|---|
getUrls() | Execute selection; returns string[] |
getUrl() | Returns the first selected URL or undefined |
limit(n) | Override max relay count for this scenario |
weight(scale) | Multiply all selection weights by scale |
policy(fn) | Set fallback policy |
allowLocal(bool) | Allow ws://localhost / ws://127.* URLs (default: false) |
allowOnion(bool) | Allow .onion URLs (default: false) |
allowInsecure(bool) | Allow plain ws:// non-onion URLs (default: false) |
filter(fn) | Filter the internal Selection[] |
update(fn) | Map over the internal Selection[] |
Fallback Policies
Applied after relay scoring when not enough relays are found. Draw from getDefaultRelays.
| Export | Behavior |
|---|
addNoFallbacks | Never add fallbacks (default) |
addMinimalFallbacks | Add 1 fallback only if zero relays were selected |
addMaximalFallbacks | Fill remaining slots up to the limit |
Filter Selection
| Export | Description |
|---|
getFilterSelections(filters, rules?) | Returns RelaysAndFilters[] — optimized relay+filter combos for a subscription |
RelaysAndFilters | { relays: string[], filters: Filter[] } |
defaultFilterSelectionRules | The default ordered rule array |
getFilterSelectionsForSearch | Rule: search filters → search relays (weight 10) |
getFilterSelectionsForWraps | Rule: kind-1059 wraps without authors → user messaging relays |
getFilterSelectionsForIndexedKinds | Rule: kinds 0/3/10002/10050 → indexer relays |
getFilterSelectionsForAuthors | Rule: author filters → each author's outbox (split into up to 30 chunks) |
getFilterSelectionsForUser | Rule: low-weight (0.2) baseline that always fires for every filter → user's read relays. It is not conditional on other rules failing. |
Other Exports
| Export | Description |
|---|
INDEXED_KINDS | [PROFILE, RELAYS, MESSAGING_RELAYS, FOLLOWS] — kinds routed to indexers |
makeSelection(relays, weight?) | Create a Selection object; validates and normalizes URLs |
Selection | { weight: number, relays: string[] } |
FallbackPolicy | (count: number, limit: number) => number |
routerContext | The global mutable options object updated by Router.configure() |
Common Patterns
1. Configure once at app startup
Router.get() is the primary entry point — it returns a Router instance using the global routerContext. Call Router.configure() once at startup to set options, or assign directly to routerContext for individual overrides.
import {Router} from '@welshman/router'
Router.configure({
getUserPubkey: () => myStore.userPubkey,
getPubkeyRelays: (pubkey, mode) => myStore.getRelaysForPubkey(pubkey, mode),
getDefaultRelays: () => ['wss://relay.example.com/', 'wss://relay2.example.com/'],
getIndexerRelays: () => ['wss://indexer.example.com/', 'wss://indexer2.example.com/'],
getSearchRelays: () => ['wss://search.example.com/', 'wss://search2.example.com/'],
getRelayQuality: (url) => myStore.getRelayQuality(url),
getLimit: () => 5,
})
When using @welshman/app, it pre-configures Router automatically. The two most common customization points when using @welshman/app are getDefaultRelays and getIndexerRelays, which you can assign directly on routerContext:
import {routerContext} from '@welshman/router'
routerContext.getDefaultRelays = () => [
'wss://relay.example.com/',
'wss://relay2.example.com/',
]
routerContext.getIndexerRelays = () => [
'wss://indexer.example.com/',
'wss://indexer2.example.com/',
]
2. Fetch events from specific pubkeys
import {Router} from '@welshman/router'
const relays = Router.get().FromPubkeys(['pubkey1', 'pubkey2']).getUrls()
3. Publish an event
import {Router} from '@welshman/router'
import type {TrustedEvent} from '@welshman/util'
function getPublishRelays(event: TrustedEvent): string[] {
return Router.get().PublishEvent(event).getUrls()
}
4. Find a quoted/referenced event with fallbacks
import {Router, addMaximalFallbacks} from '@welshman/router'
import type {TrustedEvent} from '@welshman/util'
function getQuoteRelays(event: TrustedEvent, quotedId: string, hints: string[]) {
return Router.get()
.Quote(event, quotedId, hints)
.policy(addMaximalFallbacks)
.limit(8)
.getUrls()
}
5. Common scenario cheat-sheet
import {Router} from '@welshman/router'
const router = Router.get()
router.ForUser().getUrls()
router.FromUser().getUrls()
router.ForPubkey('pubkey').getUrls()
router.FromPubkey('pubkey').getUrls()
router.Index().getUrls()
router.ForPubkey('pubkey').limit(3).getUrls()
router.merge([
router.FromUser(),
router.Index(),
]).getUrls()
6. Build subscriptions with getFilterSelections
import {getFilterSelections} from '@welshman/router'
import type {Filter} from '@welshman/util'
const filters: Filter[] = [
{kinds: [1], authors: ['pubkey1', 'pubkey2']},
{kinds: [0], search: 'bitcoin'},
]
for (const {relays, filters} of getFilterSelections(filters)) {
myPool.subscribe(relays, filters)
}
7. Use a custom filter routing rule
import {
Router,
getFilterSelections,
defaultFilterSelectionRules,
type RelaysAndFilters,
} from '@welshman/router'
import type {Filter} from '@welshman/util'
const myRule = (filter: Filter) => {
if (!filter.kinds?.includes(1)) return []
return [{filter, scenario: Router.get().FromRelays(['wss://notes.example.com/'])}]
}
const selections = getFilterSelections(filters, [myRule, ...defaultFilterSelectionRules])
Integration Notes
@welshman/util — Router imports TrustedEvent, Filter, RelayMode, PROFILE, RELAYS, MESSAGING_RELAYS, FOLLOWS, WRAP, normalizeRelayUrl, and tag-parsing helpers. All relay URLs are normalized with normalizeRelayUrl and validated with isRelayUrl before use.
@welshman/net — The default getPubkeyRelays implementation queries Repository.get() (the in-memory event store from @welshman/net) for kind-10002 events. Override it if you maintain relay lists elsewhere.
@welshman/app — The app layer pre-configures Router using its own stores (relay lists, connection quality). If you use @welshman/app, call Router.configure only to override specific options; the app layer handles the rest.
@welshman/lib — Used internally for utilities (sortBy, shuffle, uniq, etc.); no direct integration needed.
Gotchas & Tips
-
Relay list events must be in the Repository for pubkey routing to work. The default getPubkeyRelays implementation reads kind-10002 (NIP-65) relay list events from the global in-memory Repository. If those events haven't been loaded — either from a local cache at startup or fetched from the network — the Router has no relay list data for that pubkey and silently falls back to default/indexer relays. When using @welshman/app, relay lists are fetched automatically as part of profile loading (loadRelayList, makeOutboxLoader). Without @welshman/app, you must fetch and load them yourself before calling pubkey-based scenarios.
-
For* vs From*: ForPubkey returns a pubkey's read relays (where you send things for that pubkey to receive); FromPubkey returns their write relays (their outbox, where their events live). Use From* to fetch events, For* to deliver events.
-
Default limit is 3. Set getLimit in Router.configure or call .limit(n) on a scenario if you need more. PublishEvent unconditionally overrides to 30.
-
Scoring includes randomness. getUrls() introduces Math.random() in the scoring formula so that lower-quality or less-popular relays get occasional selection. Results are not deterministic across calls.
-
addNoFallbacks is the default policy. If no relays are found for a scenario (e.g. no relay list for a pubkey) and you haven't set a policy, getUrls() returns []. Use addMinimalFallbacks or addMaximalFallbacks when you need a result even for unknown pubkeys.
-
Insecure ws:// URLs are filtered by default. Only onion addresses (*.onion) are exempt from the TLS requirement. Pass .allowInsecure(true) to a scenario if you need to support plain websocket relays (e.g. local dev).
-
getFilterSelections uses addMinimalFallbacks. Each resulting relay group will have at least one relay if getDefaultRelays is configured and returns relays. If getDefaultRelays is not configured or returns an empty array, the group may still be empty.
-
routerContext is a shared mutable object. Router.configure() mutates it in place with Object.assign. new Router(options) merges the supplied options over the global routerContext (via mergeLeft), so any options not provided in options still fall back to whatever is in routerContext. For true isolation (e.g. in tests), pass a complete options object or reset routerContext first.
-
Quote reads relay hints from event tags. It looks for a tag whose second element (t[1]) matches the quoted event ID, then extracts a relay hint from t[2] and an author pubkey from t[3]. Standard NIP-21/NIP-10 tag format.