Working patterns for common PeerNet use cases. Every snippet here is exercised by the test suite — if anything stops working, the test file referenced in each section will catch it.
Two-node setup with manual addressing
For tests, scripted environments, or any case where peers know each other's address by other means.
# In your supervision tree:
children = [
{PeerNet, [name: :alice, data_dir: "/var/lib/myapp/alice", port: 7100]},
]
# Pair with a peer (out of band — typically QR-scanned):
:ok = PeerNet.pair(:alice, peer_pubkey, label: "Bob's laptop")
# Expose a handle:
:ok = PeerNet.expose(:alice, :greet, fn _from, name -> "hi #{name}" end)
# Open a connection:
:ok = PeerNet.connect(:alice, peer_pubkey, {192, 168, 1, 50}, 7100)
# Wait for handshake to complete (test pattern):
true = PeerNet.connected?(:alice, peer_pubkey)
# Call:
{:ok, "hi world"} = PeerNet.call(:alice, peer_pubkey, :greet, "world")See test/peer_net/integration_test.exs.
Two-node setup with auto-discovery (UDP)
For walkie-talkie demos on a trusted LAN.
children = [
{PeerNet,
[
name: :alice,
data_dir: "/var/lib/myapp/alice",
port: 0, # OS-assigned ephemeral
discovery: PeerNet.Discovery.UDP,
discovery_opts: [listen_port: 4040] # default; override per app
]}
]
# Pair both ways out-of-band (QR scan, typed pubkeys, etc).
:ok = PeerNet.pair(:alice, bob_pubkey)
# Expose handles:
:ok = PeerNet.expose(:alice, :chat, fn _from, %{text: text} ->
IO.puts("got: #{text}")
:ok
end)
# That's it. The Discovery.UDP process broadcasts every 5 seconds;
# Bob's matching instance hears the announce, the Registry sees a
# trusted-peer event, and dials. After ~5-10s the connection is up.
PeerNet.connected?(:alice, bob_pubkey) # true once handshake completesSee test/peer_net/discovery_integration_test.exs.
Auto-reconnect after a transient drop
No code required — built into the Registry. When a connection
process terminates (peer crashes, network blip, socket reset), the
Registry observes the :DOWN and schedules a redial with
exponential backoff (500ms → 1s → 2s → ... → 30s cap).
# Connection drops:
{:ok, conn_pid} = PeerNet.Registry.lookup_connection(:"alice.registry", bob_pubkey)
Process.exit(conn_pid, :kill)
# Wait — the Registry will redial.
# In ~500ms - 1s, this becomes true again:
PeerNet.connected?(:alice, bob_pubkey)See test/peer_net/reconnect_test.exs.
Phone controlling a Nerves device (BeamDist)
Asymmetric trust: the controlled peer (Nerves) gates access by pubkey; the controller (phone) calls without any extra ceremony.
On the controlled side (Nerves), once the controller's pubkey is known:
@controller_pubkey Base.decode16!("…32-byte hex…")
# After PeerNet.start_link in your supervision tree:
PeerNet.expose(:beam_admin, &PeerNet.BeamDist.handle/2,
authorize: fn pubkey -> pubkey == @controller_pubkey end)On the controller side (phone), once the device pubkey is known and paired:
# Synchronous: get a return value.
{:ok, status} = PeerNet.BeamDist.call(device_pubkey, MyApp.WiFi, :status, [])
# Fire-and-forget: no return value, but acks delivery on the wire.
:ok = PeerNet.BeamDist.cast(device_pubkey, Logger, :info, ["restarting"])The authorize: predicate is the entire access control. Other peers
calling :beam_admin see {:error, :forbidden}. The handler never
runs for them.
See test/peer_net/beam_dist_test.exs.
Per-row authorization
The authorize: predicate isn't limited to a single pubkey. Any
function (pubkey -> boolean) works:
admins = MapSet.new([alice_pubkey, bob_pubkey])
PeerNet.expose(:moderate, &handle_moderate/2,
authorize: fn pubkey -> MapSet.member?(admins, pubkey) end)The predicate runs on every call, so changes to the set take effect immediately (no need to revoke/re-expose).
Multiple instances in one BEAM (testing pattern)
PeerNet's name-based supervision lets you run any number of instances in the same BEAM, each fully isolated.
{:ok, _} = PeerNet.start_link(name: :a, data_dir: "tmp/a", port: 0)
{:ok, _} = PeerNet.start_link(name: :b, data_dir: "tmp/b", port: 0)
{:ok, _} = PeerNet.start_link(name: :c, data_dir: "tmp/c", port: 0)
# Each call takes the instance name as the first argument.
PeerNet.identity(:a)
PeerNet.expose(:a, :handle, fun)
PeerNet.call(:a, peer_pubkey, :handle, args)Used throughout the integration test suite.
Extracting your pubkey for QR pairing
id = PeerNet.identity(:alice)
qr_payload = Base.encode32(id.public, padding: false)
# Render qr_payload as a QR code in your UI.
# On the scanning side:
{:ok, peer_pubkey} = Base.decode32(scanned, padding: false)
:ok = PeerNet.pair(:bob, peer_pubkey, label: "Alice")Pubkeys are 32 bytes; Base32 (no padding) yields a 52-character alphanumeric string — comfortable for a QR code at any resolution.
Inspecting peer state
# All known peers (from the Registry — includes status):
PeerNet.list_peers(:alice)
#=> [%{pubkey: <<...>>, label: "Bob", added_at: ~U[2026-...]}]
# Is a specific peer connected right now?
PeerNet.connected?(:alice, peer_pubkey)
#=> true | false
# Get the live connection process (rarely needed):
{:ok, pid} = PeerNet.Registry.lookup_connection(:"alice.registry", peer_pubkey)Choosing a port
port: 0— OS-assigned ephemeral. Read it back withPeerNet.port(:alice)after start. Best for tests and one-off scripts.port: <fixed>— fixed port (e.g.7100). Best for production where you want a stable address. Make sure no other app on the host uses the same port.- For mobile (mob), the port is opaque to the user — discovery handles the address sharing.
What NOT to do
- Don't share keyfiles between hosts. Each PeerNet instance needs its own identity. Two hosts sharing the same private key are the same peer from the network's perspective; the second one will conflict with the first.
- Don't use the same
data_dirfor two PeerNet instances. Trust list and identity collide. - Don't
expose/4something with noauthorize:if it does anything destructive. Default-deny applies to unknown handles; once youexpose, every paired peer can call it. Useauthorize:to narrow. - Don't call
PeerNet.connect/4if you have UDP discovery running. Discovery + Registry will dial trusted peers automatically. Manualconnect/4is for setups without discovery.