Routing & Tags
The Router
app.use(Router) is the app's relay router. It resolves routes with:
- the user's pubkey from
app.user - read, write and NIP-17 messaging relays per pubkey from
RelayListsandMessagingRelayLists - relay quality from
RelayStats - default, indexer and search relays from
AppConfig
Routing is declarative. Code describes sources as RelaySelections ("the author's outbox", "this pubkey's inbox", "the relays this event was seen on"), and the router resolves them into concrete urls:
const router = app.use(Router)
router.resolver // a @welshman/util Resolver
router.resolve(selections) // router.resolver.scenario(selections), a Promise<RelayScenario>resolver is built with getRelayQuality from RelayStats and getDefaultRelays from AppConfig. The same resolver is injected into every domain kind by app.use(Domain), so writers and the router route alike. Router also implements @welshman/feeds' FeedRouter, which is how feed controllers route their filters.
router.resolveRoute(route) dereferences a single RelayRoute:
userInbox/userOutbox/userMessaging→ the signed-in user's read / write / NIP-17 relays, or none when signed out.pubkeyInbox/pubkeyOutbox/pubkeyMessaging→ that pubkey's relays (viaRelayLists/MessagingRelayLists).eventInbox/eventOutbox→ the relays ofref.pubkey, and of the author ofref.idwhen the repository has that event, merged with anyref.relayshints.seen→ the tracker's relays for the event id (or, for a replaceable ref, the id of the event at its address), plusref.relayshints.index→AppConfig.getIndexerRelays();search→AppConfig.getSearchRelays().relay→ the literalroute.url.
The RelaySelection DSL
@welshman/util exports constructors that build RelaySelections, each pairing a RelayRoute with a weight (default 1). relays and inboxes return arrays:
import {
inbox, outbox, messaging,
userInbox, userOutbox, userMessaging,
eventInbox, eventOutbox, seen,
relay, relays, inboxes,
indexers, searchRelays,
} from "@welshman/util"
inbox(pubkey) // that pubkey's read relays (deliver here so they receive)
outbox(pubkey) // that pubkey's write relays (their events live here)
messaging(pubkey) // that pubkey's NIP-17 relays
userOutbox() // the current user's write relays
eventOutbox(ref) // the author of a referenced event
seen(ref) // relays a given event was found on
relay(url) // a literal relay url
relays(urls) // one selection per url -> RelaySelection[]
inboxes(pubkeys, 0.5) // one weighted inbox per pubkey -> RelaySelection[]
indexers() // profile/relay-list index relays
searchRelays() // full-text search relaysA selection list resolves to a scored RelayScenario, and the scenario to urls:
import {addMinimalFallbacks} from "@welshman/util"
const scenario = await app.use(Router).resolve([
userOutbox(),
...inboxes(mentionedPubkeys, 0.5),
])
const urls = scenario.policy(addMinimalFallbacks).limit(8).getUrls()
const hint = scenario.getUrl()Scoring, the limit/policy/allow* options and the fallback policies are documented under RelayScenario.
Routes from domain writers
Each @welshman/domain writer computes its own RelaySelections. writer.scenario() resolves them through the injected resolver, and writer.relays() returns the urls. The default is the author's outbox plus the inbox of every p-tagged pubkey, and kinds override it, as described in Routing a writer.
Forced routes
A writer with forced routes publishes only there, bypassing outbox and inbox routing:
writer.forceRoutes(relay("wss://relay.example.com/")) // publish only here
writer.forceRoutes(userInbox()) // any route, resolved at render
writer.setRoom("wss://rooms.example.com/", roomId) // forced route plus an "h" tagNIP-29 room ops and relay-management ops require explicit relays, so render() throws unless setRoom or forceRoutes was called.
Relay quality
app.use(RelayStats) collects per-relay connection statistics (open, close, publish, request and event counts, timestamps, recent errors) and exposes the quality score the router ranks relays by.
const stats = app.use(RelayStats)
stats.one(url) // Readable<Maybe<RelayStatsItem>>
stats.getQuality(url) // number in [0, 1] — 0 for blocked/error-prone relaysThe appPolicyRelayStats default policy fills the stats. getQuality returns 0 for non-relay urls, relays in the user's blocked list, and error-prone relays, and higher scores for relays that are connected or have been seen before. The scenario drops a relay that scores 0.
Tags & relay hints
Domain writers build their own tags, including relay hints. A helper that adds a tag with a relay hint resolves the hint through the resolver in the background, and renderTags() waits for it before returning. The helpers are listed under EventWriter. A typical reply:
import {Note} from "@welshman/domain"
const writer = app.use(Domain)
.writer(Note)
.setContent("well said")
.setParent(parentEvent) // NIP-10 threading, with relay hints
const command = await app.use(Domain).command(writer)
await command.publish()