Skip to main content

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:
The rule in the router must point at the same LAN port the host listens on, so pin 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. Use lan-and-relay with 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.portForwarded as above.
In every case 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

It runs the same discovery the host runs at startup and prints what each protocol answered:

“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.publicHost to a dynamic-DNS name, or pairing.bootstrapUrl to 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.