# 0022. Finding the hub on the network

- Status: Accepted
- Date: 2026-10-28

## Context

A hub on a shop's LAN usually gets its IP address from the router. After a power cut or a router
change, the address can change, and every paired device loses the hub until someone types the
new address into each one (**Change hub address**, M7). The hub already announces itself over
mDNS as `_openkedai._tcp` (ADR 0019), but nothing listened.

Following a hub automatically has a risk that typing an address doesn't: a device follows
whatever answers on the network. Anyone on the shop's Wi-Fi can announce a service with any
name. A device that sent its token to such a service would hand over the shop's access.

## Decision

**The hub's identity.**
- Each hub makes a random **hub id** once and keeps it in its registry (`hub_state.hub_id`).
- The id is in the hub's mDNS TXT record (`id`, next to `api`), in `GET /api/hub`, in the
  credentials from claiming, pairing and rejoining, and in `GET /api/devices/me`.
- Devices store it with the hub's address (`hub.id`). It is optional in the protocol, so
  devices paired before M8.5 learn it the first time they follow the hub.

**Proving the hub is the device's own, before the token is sent.**
- The hub stores only the SHA-256 of each device token. A device that finds a candidate hub
  sends `POST /api/hub/prove` with its device id and a fresh random nonce (no token).
- The hub answers with `HMAC-SHA256(key = token hash, message = nonce)` for each working token
  of that device. The device computes the same from its own token and compares.
- Only a hub that holds the token's hash can answer. A new nonce each time means an answer
  can't be replayed. A removed device has no working token, so its old hub can't prove anything.
- Only after the proof does the device send its token, to `GET /api/devices/me`. It follows the
  hub if the answer names the same shop and device. This is the same check as **Change hub
  address**.

**When a device looks.**
- The app feeds the sync engine's status to a `HubFollower`. It starts looking when the hub
  stops answering, or when the hub at the old address doesn't know the device (another hub took
  its address).
- It looks 5 seconds after the hub is lost, then every 20 seconds while it stays lost. Each look
  browses for 4 seconds. A device follows a restarted hub within about 30 seconds of it
  answering again.
- It considers only hubs advertising its hub id. A device without a stored id considers every
  hub that has one, and the proof filters the rest.
- When the hub is still at its old address, the device just syncs again at once.
- It doesn't look for a hub whose address is `https://`. Such a hub is on the internet or
  behind a proxy with a fixed name, so it doesn't move around a LAN.

**Native code.** A new Expo module, `openkedai-discovery`, has one call:
`browse(type, timeoutMs)`. It lists the services found, each with its IPv4 addresses, port and
TXT record.
- **iOS:** Network.framework's `NWBrowser`, resolving each service by connecting to it over
  IPv4. The app declares `NSBonjourServices: ["_openkedai._tcp"]` for iOS's local network
  permission.
- **Android:** `NsdManager`, resolving one service at a time (older Android allows only one at a
  time).
- **Web preview and Expo Go:** browsing isn't available, and the app simply doesn't look.

**Joining.** The join screen and **Change hub address** list the hubs found on the network.
Tapping one fills in its address; the pairing code is still needed.

## Consequences

- **Hub IP changes:** devices reconnect on their own, with no one touching them. A fixed address
  in the router is no longer needed, though it is still a good idea.
- **Spoofed hubs:** something announcing the shop's hub id gets a nonce, never a token.
  - A copy of the hub's registry (`hub.sqlite`) could pass the proof. Whoever has that file has
    the hub's data anyway.
  - Plain HTTP on the LAN can still be watched, as ADR 0019 says.
- **Network limits:** mDNS doesn't cross routers or VLANs, and some guest Wi-Fi networks block it.
  There, devices keep retrying the old address, and **Change hub address** remains the way to
  move them.
- **Android emulator:** the emulator's own network usually doesn't receive the Mac's mDNS, so
  checking discovery on Android may need a real device on the same Wi-Fi.
- **Old hubs:** devices can't follow a hub older than M8.5. Its info has no hub id, and it has no
  prove route.
- **Hub port:** the hub now advertises the port it actually listens on, not the configured
  one. They differ only when the configured port is 0.
