Skip to content

0019. Hub sync and pairing

  • Status: Accepted
  • Date: 2026-10-27

In hub mode several POS devices share one shop: a waiter’s phone opens a table that the counter tablet charges, and every device sees held orders as they change. The hub keeps the canonical log (ADR 0004), but devices must keep selling when it is unreachable (ADR 0002). A shop usually starts on one device in single-device mode and adds a hub later, with no data loss. The hub may sit on a shop LAN with no TLS, and before M7 there is no back office to show a pairing QR or to sign in to.

Adding a shop to a hub. One hub can serve several separate shops (tenants), each in its own SQLite file; a device’s token ties it to one shop, so shops never see each other. The hub prints a setup code, the hub owner’s code for adding shops, in its log on every start (OPENKEDAI_SETUP_CODE sets one). A shop’s first device adds the shop by entering the hub’s address and the setup code (“Move this shop to a hub”). It uploads its outlet, catalog, staff and devices, then its whole event log through the normal outbox. The hub’s copy of the shop takes the device’s tenantId and outletId, so no event is rewritten. Several outlets of one business in one shop come later; every row and event already carries its outlet.

Pairing more devices. An owner or manager on a paired device taps Pair a device. The hub issues a one-time pairing code (8 characters, single use, valid for 10 minutes), shown as a QR code with the hub’s addresses and as text to type. The new device chooses Join a hub during setup, scans or types the code, and gets a device token (32 random bytes; the hub stores only its hash). A device that already has its own sales can’t join another shop’s hub: events can’t be rewritten into another tenant, and nothing is deleted.

Device trust. Every API call and the live connection carry the device token. A paired device is trusted to act for its shop; the app decides which staff may do what, as in single-device mode. Owner sign-in in a browser comes with the back office (M7).

Events.

  • Devices push their own events from an outbox (events this device wrote after its push cursor) in gzip-compressed batches to POST /api/events. The hub validates them, rejects events for another tenant or written by another device, and stores them by id (INSERT … ON CONFLICT DO NOTHING), so a resent batch never duplicates.
  • Devices pull every event of the shop after their hub cursor with GET /api/events and store them locally the same way. Each device replays orders and shifts with the same packages/core code, so all devices converge on the hub’s state (ADR 0007, ADR 0015).
  • A WebSocket (/api/live) only says that something changed (new events up to a cursor, or a new catalog revision); devices then pull. Missed messages cost nothing: devices also pull on reconnect and after every push.

Catalog, staff and settings stay versioned rows, not events.

  • A device pushes the rows it changed since its last push.
  • The hub keeps an incoming row only if its version is higher than the hub’s, and bumps the shop’s catalog revision.
  • Devices fetch the whole snapshot when the revision changes and keep a local row only while it is newer than the hub’s, i.e. not yet pushed.
  • When two devices change the same row offline, the first to reach the hub wins.
  • Device rows sync only their code and name; receipt counters stay on the device.

Discovery. The hub advertises itself over mDNS as _openkedai._tcp (on by default, OPENKEDAI_MDNS=off turns it off). The pairing QR carries every LAN address of the hub, so devices don’t need discovery to pair. (Since M8.5, devices also use it to follow a hub whose address changed; see ADR 0022.)

Versions. GET /api/hub returns the API version. The app refuses to sync with a hub whose major version differs and says which side to update.

  • The hub keeps a small registry database (<data>/hub.sqlite): shops, device token hashes, pairing codes and the setup code.
  • Anyone with the setup code can add a shop to the hub, so it is for the person who runs the server, not for shop staff. A public plugin route on a hub with several shops names the shop with ?shop=<tenant id>.
  • Plain HTTP on a LAN means anyone on the network can watch traffic. The guidance for remote access (HTTPS through Caddy, Cloudflare Tunnel or Tailscale) stays as in the brief.
  • Catalog edits made offline on two devices can overwrite each other; sales never do.
  • A stolen paired device can act for the shop until its token is revoked; revoking devices comes with the back office.

For AI tools: llms.txt ·llms-full.txtopenkedai.com