Authenticated Pouring¶
How a token presented at a Kegboard becomes an open valve and an attributed
pour. This document specifies the interaction between device and server; the
message envelopes it uses (token events, commands, command_result) are
defined in the main protocol doc.
1. Background¶
A Kegboard install always has flow meters on its beer lines: every pour is measured, whether or not anyone is identified. Many installs add more, per tap: a solenoid valve on a relay, so beer only flows for someone authorized, and a token reader nearby (RFID, iButton, …) to identify who that is. Others have no valves at all — anyone can pour at any time, and authentication only decides who a pour is attributed to.
Everything in this document is optional. A monitoring-only Kegboard — just meters, no readers, no valves — needs none of it: it simply reports guest pours, and its config never mentions authentication at all.
A presented token means something — which user, which taps, how much — and the token→user database that answers this can be large. It lives on the server; the valves and the pour detection live on the device. This doc defines who decides what, and how the two sides stay simple.
2. Summary¶
A pour is metered flow, reported to the server as an event (main doc §5.1) — authenticated or not.
A grant is permission to pour. The server creates one with an
authorizecommand, naming the meters it covers, the relays (valves) it opens, and its limits: max volume, max total time, max idle time. One active grant per meter.Presenting a physical auth token sends a
tokenevent to the server. In response, the server decides whether to grant access (authorizecommand), opening any relevant valves; or to deny it (denycommand).Pours are tagged with the covering grant’s id; the server maps that to a user. Identity never reaches the device.
A grant ends when a limit is hit, its token detaches, the server revokes it, or a newer grant takes its meters. Every ending is reported as a
grant_endevent with its reason.Server unreachable? A configured offline policy (§6) decides whether the user is signaled a refusal — either way, nothing opens, and the queued
tokenevent preserves the audit trail.
The rest of this doc unpacks each of these.
4. The core flow¶
Token presented. The reader reports a token; the device emits a
tokenevent withaction: "attached"— always a question for the server. The device flushes the batch immediately.Server decides. The server looks up the token, applies whatever policy it likes (user standing, keg access, time of day), and responds — in the same HTTP response — with an
authorizeor adenycommand. Servers SHOULD always answer a decision-requesting token event with one of the two.Device acts. On
authorize, the device opens the relays named in the grant, tags subsequent pours on the granted meters with the grant, and acknowledges with acommand_resultevent. Ondeny, the tap stays closed and the device SHOULD signal the user (e.g. a refusal tone). If the response carries neither — a server bug, defensively — the device treats the presentment as denied, without the user signal.Grant ends by whichever comes first: token detach (presence readers), any of the grant’s limits — volume poured, total time, idle time — being reached, or a server
deauthorizecommand. The device closes the valve, ends any in-flight pour (still tagged with the departing grant), clears the grant, and reports agrant_endevent naming the reason (main doc §5.7), so the server never has to guess why a tap went cold.
Identity never travels down. The server attributes the resulting pours
itself, from each pour’s grant_id — the server’s own identifier for the
grant (main doc §5.1, §7.1). The device acts on tokens and grants; it
never knows who a user is.
Authorization latency is one HTTP round trip, because the decision rides the response to the token event itself:
reader device server
| | |
| token 0089f2c4 | |
|------------------>| POST /kegboard-event |
| | [token attached] ---------------->|
| | | lookup, policy
| | 200 {commands:[authorize]} |
| |<------------------------------------|
| | open valve(s), record grant |
| | |
| | ...pour happens... |
| | POST [pour grant_id=g_1, |
| | command_result ok] ---------->|
5. Commands¶
The three commands this flow uses are fully specified in the main protocol doc (§7.1–7.3); this section describes only their role in the flow.
authorizecarries exactly one grant: a server-assigned id, the meters it covers and the relays it opens (if any). The server composes these sets from its own meter and relay configuration, and any limits. The device applies it verbatim: relays open, and pours on the granted meters are tagged with the grant, until a limit (volume, total time, idle time) or the device’s own clamp ends it. Several grants at once — different taps, different policy — are simply severalauthorizecommands in one response.denyis the explicit refusal: the device signals the user, and no state changes.deauthorizeis a server-initiated cutoff, revoking grants by id — an admin button, a policy engine, an emergency stop. Detach and the grant’s own limits do the same thing device-side without a command.
6. Offline behavior¶
The decision-maker being remote means presentment can race an outage. The
device applies a configured offline_policy when a token event cannot be
delivered (network error / 5xx / no response before a short timeout,
suggested 5 s):
|
Behavior |
|---|---|
|
Tap stays closed; the device signals refusal. Correct for installs where gating is the point. |
|
Nothing opens and nothing is granted — the only difference from |
Note what is deliberately absent: a device-side token cache. Caching assignments would reintroduce the state this design removes and creates stale-revocation problems.
7. Multi-meter, multi-user¶
Grants are per meter, so a two-tap device can simultaneously have Alice on meter 0 and Bob on meter 1:
Alice presents; the server responds
authorize {grant_id: "g_1", meter_numbers: [0], relay_numbers: [0], ...}, recordingg_1as Alice’s.Bob presents; the server responds
authorize {grant_id: "g_2", meter_numbers: [1], relay_numbers: [1], ...}, recordingg_2as Bob’s.Each meter’s pours arrive tagged with their own grant’s
grant_id, so the server attributes meter 0’s pours to Alice and meter 1’s to Bob. Detach, limits, anddeauthorizeaffect only their own grant’s meters.
Which meter a presentment maps to is server policy, not protocol. The
token event tells the server which reader saw the token (auth_device);
an install with one reader per tap can name readers accordingly (e.g.
core.rfid.0) and the server maps reader → meter. An install with one
shared reader can grant all meters, or apply fancier policy (the user’s
reserved tap, the tap with their keg on it). The protocol only carries the
outcome: each grant’s meter_numbers and relay_numbers.
8. Grant and pour corner cases¶
Every way a pour and a grant can interact, and the rule for each. Two principles cover them all:
Attribution is decided when the pour ends. The pour is tagged (
auth_device,auth_token,grant_id— main doc §5.1) with the grant covering its meter at that moment, in full. The server resolves the user fromgrant_id— pinned to its own authorization decision, so a late-delivered pour attributes correctly even if the token was reassigned in the meantime.Limit accounting is decided as flow is observed. Flow counts toward the grant covering the meter at the moment it flows — toward its
max_volume_ml, and resetting itsmax_idle_ms— and is never retroactive.
Where the two disagree (case 2 below), that is deliberate: the rules stay simple, and the mismatch is confined to a corner. In general this catalog favors the simple implementation over the clever one, accepting that a handful of corner cases do slightly surprising things.
Pour with no grant. An unauthenticated guest pour: no auth fields at all. This is every pour in
openmode, and any pour on a meter nobody has authorized — installs without valves meter everything, all the time.Grant arrives mid-pour: it adopts the pour. The pour keeps its
pour_idand, at its end, is attributed to the grant in full, including the volume poured before the grant arrived. This is the headline corner case on valve-less installs: someone starts pouring, realizes they forgot to authenticate, and keys in mid-glass — the whole glass lands on their tab. Per the second principle, the pre-grant volume does not count toward the grant’s limits. Adoption is a single, clearly marked policy point in the firmware, so it can become “split into a new pour” later without disturbing anything else.Grant replaces another mid-pour: the pour splits. The in-flight pour ends immediately, tagged with the departing grant; flow that continues opens a new pour (fresh
pour_id) under the new grant. Pour boundaries always align with authorization boundaries, so consecutive drinkers never share a pour record.Grant updated (same
grant_id) mid-pour: nothing happens. It is the same grant; the pour continues under it, counters intact.Grant ends mid-pour — limit reached, token detached,
deauthorize: the pour ends first, tagged with the grant, and its event precedes thegrant_end(main doc §5.7). Flow that continues — after the valve closes, or on a meter that never had one — is case 1 again: a new, unauthenticated guest pour.