1# Pair two homes23<!-- Source: FelineStateMachine/lofi-node README.md, src/tunnel.ts, src/node.ts (pair/status). -->45Two nodes in two places — a home server and a studio machine, say — can hold the same store and6converge over [iroh](https://iroh.computer): dialed by public key, hole-punched through NATs, no7static IPs, no port forwarding, no cloud dependency. Devices keep syncing against whichever node is8closest; the nodes reconcile with each other.910## Pairing1112Every node with the mesh up prints a **node-pairing ticket** on start — an iroh `endpoint…` string,13distinct from the `lofisync1.` app tickets users enroll. On the second node:1415```sh16lofi-node pair endpoint… --dir ./data17```1819or at runtime, without a restart and without the public port changing:2021```ts22await node.pair(otherNodeTicket);23```2425Pairing is an **upstream election**: the paired node becomes a leaf that forwards to the root. Sync26WebSockets and catalogue HTTP (schema reads, deploys) tunnel over iroh, so administering either node27reaches the root — [provisioning through a leaf](provision-a-store.md) lands on the root's store,28verified end to end in lofi-node's test suite.2930## What the tunnel carries3132One iroh connection carries exactly one WebSocket or one HTTP request. Each connection opens with a33small HELLO frame naming what it carries (`ws` with path and subprotocol, or `http` with method,34path, and headers); frames then bridge both ways until either side closes. This is why the35browser-facing protocol never changes: the leaf re-originates exactly what Jazz expects.3637## Observing the mesh3839```ts40node.status().mesh;41// { state: "up", nodeId, ticket, connections: [{ direction, rtt, paths }] }42```4344`connections` reports each live tunnel with its round-trip time and path counts — direct versus45relay — so you can see whether hole-punching succeeded or traffic is riding an iroh relay. The other46two states are honest degradations: `{ state: "off" }` when the mesh is disabled, and47`{ state: "unavailable", reason }` when the native layer could not load — the Jazz server still runs48LAN-only, and `ticket()`/`pair()` throw `MeshUnavailableError` rather than pretending.4950## The relay's role5152Relays assist that hole-punching (address discovery) and carry traffic only for the connections that53cannot go direct; in practice the large majority of iroh connections are direct. The relay is a54**per-node election** (`relay` in [config.json](configuration.md)): whichever relay a node elects55travels inside its pairing ticket, so the dialing side needs no matching configuration. The default56is n0-computer's public relays, which are rate-limited and meant for development. For a production57mesh, [bring your own](beyond-the-lan.md).5859## Choosing a root6061The root holds the authoritative store; leaves relay to it. Put the root on the machine with the62most reliable storage ([storage choices](storage-choices.md)) and pair outward from there. A leaf's63election can be redone at runtime (`pair` again) if the topology changes.