14 KiB
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
# or
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()
// relays is string[] — pass to your subscription
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()
// Automatically includes author's outbox + mentioned pubkeys' read relays
// Hard-limited to 30 relays for deliverability
}
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()
// Read relays for the current user (where others deliver events to you)
router.ForUser().getUrls()
// Write relays for the current user (your outbox)
router.FromUser().getUrls()
// Best relays to deliver an event to a pubkey (their inbox)
router.ForPubkey('pubkey').getUrls()
// Best relays to fetch events authored by a pubkey (their outbox)
router.FromPubkey('pubkey').getUrls()
// Indexer relays (profiles, relay lists)
router.Index().getUrls()
// Cap relay count for this scenario only
router.ForPubkey('pubkey').limit(3).getUrls()
// Merge multiple scenarios; relay URLs are deduplicated when getUrls() is called
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)) {
// Open one subscription per relay group
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'
// Add a rule that sends kind-1 to a dedicated relay
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 importsTrustedEvent,Filter,RelayMode,PROFILE,RELAYS,MESSAGING_RELAYS,FOLLOWS,WRAP,normalizeRelayUrl, and tag-parsing helpers. All relay URLs are normalized withnormalizeRelayUrland validated withisRelayUrlbefore use.@welshman/net— The defaultgetPubkeyRelaysimplementation queriesRepository.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-configuresRouterusing its own stores (relay lists, connection quality). If you use@welshman/app, callRouter.configureonly 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
getPubkeyRelaysimplementation reads kind-10002 (NIP-65) relay list events from the global in-memoryRepository. 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*vsFrom*:ForPubkeyreturns a pubkey's read relays (where you send things for that pubkey to receive);FromPubkeyreturns their write relays (their outbox, where their events live). UseFrom*to fetch events,For*to deliver events. -
Default limit is 3. Set
getLimitinRouter.configureor call.limit(n)on a scenario if you need more.PublishEventunconditionally overrides to 30. -
Scoring includes randomness.
getUrls()introducesMath.random()in the scoring formula so that lower-quality or less-popular relays get occasional selection. Results are not deterministic across calls. -
addNoFallbacksis 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[]. UseaddMinimalFallbacksoraddMaximalFallbackswhen 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). -
getFilterSelectionsusesaddMinimalFallbacks. Each resulting relay group will have at least one relay ifgetDefaultRelaysis configured and returns relays. IfgetDefaultRelaysis not configured or returns an empty array, the group may still be empty. -
routerContextis a shared mutable object.Router.configure()mutates it in place withObject.assign.new Router(options)merges the supplied options over the globalrouterContext(viamergeLeft), so any options not provided inoptionsstill fall back to whatever is inrouterContext. For true isolation (e.g. in tests), pass a complete options object or resetrouterContextfirst. -
Quotereads 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 fromt[2]and an author pubkey fromt[3]. Standard NIP-21/NIP-10 tag format.