Two mechanisms for discovering other iroh-http nodes:
- DNS discovery — global, always-on. Any node that publishes its address via Pkarr can be resolved by public key using standard DNS.
- mDNS — local network. Nodes announce their presence on the LAN and can
find each other without internet connectivity, via
node.advertisePeer()andnode.browsePeers().
DNS discovery is enabled by default and configured at node creation:
await createNode({
discovery: {
// Default: true — uses n0's hosted DNS infrastructure.
dns: true,
// Custom resolver:
// dns: { serverUrl: 'https://dns.example.com' },
// Disable entirely (air-gapped / embedded):
// dns: false,
},
});When enabled, node startup automatically publishes a signed Pkarr record
containing the node's relay URL and direct socket addresses. On node.fetch,
if the peer's address isn't already known, Iroh resolves it via DNS before
the QUIC handshake — transparently, with no extra code.
Announce this node as a discoverable iroh-http peer on the local network via mDNS until the signal fires:
const controller = new AbortController();
node.advertisePeer({ serviceName: 'my-app', signal: controller.signal });
// Stop advertising:
controller.abort();Returns a Promise<void> that resolves when advertising stops. Calling it
without a signal advertises until the node is closed.
Discover peers on the local network as an async iterable:
for await (const event of node.browsePeers({ serviceName: 'my-app' })) {
if (event.isActive) {
console.log('found peer:', event.nodeId, event.addrs);
} else {
console.log('peer left:', event.nodeId);
}
}See PeerDiscoveryEvent in the specification for the event shape.
Cancel by passing an AbortSignal or by breaking from the loop — both clean
up the underlying mDNS listener:
const controller = new AbortController();
for await (const event of node.browsePeers({ signal: controller.signal })) { ... }
controller.abort();
// Or just break:
for await (const event of node.browsePeers({ serviceName: 'my-app' })) {
if (done) break;
}See MdnsOptions in the specification for the option shape.
browsePeers and advertisePeer accept MdnsOptions. Both can run
simultaneously on the same node — they are independent.
node.advertisePeer() / node.browsePeers() are the ergonomic, zero-config
path for finding iroh-http peers: the instance name is the node id, the port
comes from the endpoint, and the TXT set is fixed (pk + optional relay).
To advertise or browse any DNS-SD service — a printer, a game lobby, a
non-iroh daemon — use the generic node.advertise() / node.browse()
primitives. They are the same wire protocol and the same underlying engine, but
lossless and fully caller-controlled:
const ac = new AbortController();
// Advertise an arbitrary service.
await node.advertise({
serviceName: "printers", // → _printers._tcp.local.
instanceName: "Front Desk",
port: 9100,
protocol: "tcp", // "udp" (default) | "tcp"
txt: { model: "LaserJet 9000", color: "true" },
signal: ac.signal,
});
// Browse — records are lossless: instance, host, port, addrs, and every TXT key.
for await (const rec of node.browse({ serviceName: "printers", protocol: "tcp" })) {
console.log(rec.isActive ? "up" : "down", rec.instanceName, rec.port, rec.txt);
}node.advertisePeer() / node.browsePeers() are defined as the iroh-http
specialization of these generic primitives — there is one bridge, not two. The
generic surface lives on the node because the native discovery FFI is loaded
through the node addon (per ADR-018); it
does not otherwise use the node's identity or endpoint.
iroh-http advertises under a known service name with the node id in a pk TXT
property. asIrohPeer(record) reinterprets a generic ServiceRecord as a
DiscoveredPeer when it carries one, and returns null otherwise:
import { asIrohPeer, IROH_HTTP_SERVICE } from "@momics/iroh-http-node";
for await (const rec of node.browse({ serviceName: IROH_HTTP_SERVICE })) {
const peer = asIrohPeer(rec);
if (peer) await node.fetch(`httpi://${peer.nodeId}/api`);
}iroh-http discovery is not interoperable with plain-iroh's built-in mDNS
(swarm-discovery / iroh-mdns-address-lookup). The two use different wire
formats: iroh-http speaks standard DNS-SD with a PTR record so it is browsable
by Bonjour, iOS NWBrowser, and Android NsdManager, whereas iroh's own
backend omits PTR and is invisible to those browsers. This trade — dropping
plain-iroh mDNS interop to gain standards-compliant, mobile-visible discovery —
is the whole point of the change; see
ADR-017 for the wire-format decision.
Nodes on the same LAN still connect fine regardless: once a peer's node id and addresses are known (via DNS discovery, a ticket, or a direct address), the QUIC handshake is identical. Only the mDNS enumeration differs.
When DNS is disabled and neither browse nor advertise is called, the node
operates in explicit-address mode. Connections must use direct hints
(directAddrs), a home-relay hint (relayUrl), or ticket strings (see
tickets).
Appropriate for embedded targets, air-gapped networks, and integration tests.
| Feature | Node / Deno / Tauri |
|---|---|
| DNS discovery (auto-resolve by public key) | ✅ |
advertise() |
✅ (AbortSignal) |
browse() |
✅ (async iterable + AbortSignal) |
Feature flag: mDNS browse and advertise require the
mdnscompile-time feature in all Rust adapters.
Discovery speaks standard DNS-SD
over mDNS (PTR + SRV + TXT + A/AAAA), so a desktop node is browsable
by Apple's NWBrowser and Android's NsdManager — see
ADR-017 for why this wire format was
chosen.
Tauri apps must declare local-network permissions and Bonjour service types
before mDNS works on a device. See
Mobile mDNS / DNS-SD setup for the exact
iOS Info.plist and Android AndroidManifest.xml entries.
Mobile has one native generic browse engine and one native generic advertisement
engine. The peer APIs use those same adapters, then apply the iroh-specific TXT,
identity, and endpoint-lookup projection in Rust. Android resolves full generic
records (host, port, TXT, addresses); iOS surfaces the instance name, service
type and TXT but leaves host/port/addresses unresolved (NWBrowser does not
resolve endpoints without an NWConnection). A single iroh-http:discovery
capability permission grants both paths.