Remote Access
networkMode: "remote" asks your router for an inbound port and advertises the
resulting public address in the pairing QR. A phone that scanned at home keeps
working on cellular, and the desktop app needs no tunnel provider, no account,
and no manual port forwarding.
How the port is obtained
The host tries three router protocols, in order, and stops at the first that works:
Independently, a STUN query (RFC 5389) discovers the address the internet sees.
When STUN and the router disagree, the router wins: a VPN or a service like
Cloudflare WARP changes what STUN reports while the router’s own view of its WAN
interface remains the address a mapping actually lands on.
Some routers accept only permanent mappings and reject a lease with UPnP error
725 OnlyPermanentLeasesSupported. The host retries with a zero lease rather
than giving up, and releases the mapping on shutdown, so “permanent” does not
mean left behind.
While the host runs, the mapping is renewed at 75% of its lease.
One port, on purpose
The bootstrap page the phone installs and the transport it connects over are served on the same port. One port means one mapping: there is no second forwarded port to get wrong, and no page that loads over a route the socket cannot use. The port is also remembered across restarts (listen-port.json in the host’s
storage directory), so a stored endpoint on a paired phone stays valid after the
desktop app — or the whole machine — restarts.
Turning it on after startup
networkMode is often a runtime choice — a “reachable from anywhere” switch in
a settings panel, not a constant in the source. Apply it with
await server.setNetworkMode("remote"). Switching back with
await server.setNetworkMode("local-only") releases the router mapping and
disconnects signaling and relay reachability instead of merely hiding those
routes from the next QR. server.enableRemoteAccess() is the lower-level probe
for a UI that wants router diagnostics without changing the selected mode.
The LAN listener must already be bound to all interfaces (lan: { bind: "all" })
for this to work — a mapping pointed at a loopback-only socket connects and then
refuses everything. Hosts started in networkMode: "remote" get that binding
automatically; hosts that may switch later should set it themselves.
When the router refuses: forward the port yourself
A router with UPnP and NAT-PMP disabled cannot be talked into a mapping, but it can still forward a port if you add the rule in its admin page by hand. Tell the host that you did — it then advertises the public address on your word, since no protocol confirmed anything:lan.port rather than letting it float — a forward aimed at last week’s
ephemeral port silently reaches nothing.
remote.publicHost is the part that makes an installed home-screen app durable.
Without it the address comes from STUN, which is correct right now and wrong the
day your ISP renews its lease; the host re-checks every 15 minutes and updates
the endpoint, but a phone that stored the old address and is off Wi-Fi has
nothing to re-pair against. A dynamic-DNS hostname that follows the address
(duckdns, your registrar’s DDNS, your router’s built-in client) keeps every
stored endpoint valid across the change. Setting either option turns remote
access on by itself — no networkMode: "remote" needed.
Nothing on this machine can verify a forward from the inside, so
getRemoteDiagnostics() reports confidence: "manual" and manual: true for
this path. Confirm it once from a phone on cellular data:
When it cannot work
Remote access depends on your network, so it can genuinely be unavailable. The host reports the reason instead of silently degrading:- Carrier-grade NAT. If your ISP hands you an address in
100.64.0.0/10, there is no public address to map and no software can create one. Uselan-and-relaywith a relay you can reach, or a provider tunnel. - Double NAT. A private address on the router’s WAN interface means another router sits in front of it; the mapping would only reach that middle network.
- The router refuses. Some routers ship with UPnP/NAT-PMP disabled. Enable
it in the router’s admin page, or forward a port by hand and use
remote.portForwardedas above.
getPairingCode() throws with the specific reason rather than
handing back a QR that quietly only works on the sofa. server.getRemoteDiagnostics()
returns the same detail as structured data, including every protocol attempt.
Checking before you blame the SDK
“The self-check did not succeed” is not a failure
Many routers cannot route a request from inside the network back to their own public address — they lack NAT hairpinning. The mapping is fine; the test is being run from the one place that cannot see it. Confirm from a phone on cellular data, not from the host machine.What this does not promise
- A stable identity forever. Your ISP can change your public address. A PWA
installed against that origin would then point at the old one; set
remote.publicHostto a dynamic-DNS name, orpairing.bootstrapUrlto a stable origin you control, if that matters to you. - A route through a network you do not control. Corporate and campus Wi-Fi usually block inbound connections outright. That is a relay’s job.