← The specification

The companion app

Rendered from spec at76d46f3 (2026-10-25) · its source · Edit this page

Status: Accepted · Audience: Both · Area: N — Companion


Purpose

A fourth surface, on the device the operator actually has on them.

The CLI, TUI and web UI all run on the machine the stack runs on. That is the right place to set a stack up and the wrong place to be when the downloads stop at eleven at night. The question this feature answers is not “can we have an app” but what a surface is allowed to be once it is no longer on the host — what it may assume, what it must prove, and what it must say when it cannot reach anything.

It is one application serving two people. Which one it is for is decided by the credential that signs in, never by which build was installed (D6).

This feature owns getting connected and staying honest about it. What the app then shows is N2 and N3; what it uses of the device is N4.

Behaviour

It is a surface, and parity binds it

The app renders the core’s answers and decides nothing the core does not already decide (ADR-0017). Every action reachable from a terminal is reachable here, with one stated exception below.

The app has one identity, and the build does not choose it

An application’s identity is what the operating system installs it as. Two builds carrying two identities are two applications: they install beside one another, each with its own storage, and neither can read the other’s.

That is why this cannot be left to whoever ran the build. The pairing and the pinned fingerprint N1-R45 refuses to discard live in that storage, so a build that arrives under a different identity does not update the operator’s app — it appears next to it, empty, while the pairing stays in the one now orphaned. The operator is told to pair again and is given no reason, because from inside either app nothing is wrong.

It is also the Purpose’s rule read at the layer beneath it. Which person the app serves is decided by the credential that signs in, never by which build was installed; an identity derived from the builder’s environment makes which build was installed a distinguishing fact about the app, which is the thing that sentence forbids being load-bearing.

So the identity is declared once, in the repository, where it is reviewable and cannot differ between two people who both ran the build. A configured identity that disagrees with the declared one is refused rather than built, and the refusal names both — an identity is not something an operator can be expected to notice the drift of by reading a screen.

Reaching a stack is the operator’s choice, not the app’s

lemonfiber’s admin surface binds to loopback and refuses to widen without authentication (C6). A phone therefore cannot reach a stack until the operator has chosen to let it, and the app must not present that choice as something it can make on their behalf.

What it does instead is explain the choice in the operator’s terms, and name what has to be true: a bound address, authentication configured, both devices on the same network.

Pairing carries an address and a credential, once

A person types as little as possible. The app is pointed at a stack by scanning a code the operator’s own surfaces can display, and falls back to typed entry where a camera is unavailable or declined.

Typed entry has no software comparison behind it, so the person makes one. The app shows a short code folded from the whole fingerprint, and the stack shows the same code beside the material it produced. Both sides fold it the same way (N1-R73), and the stack shows it wherever it shows the material (N1-R74): a code the app asks somebody to check against a screen that does not carry it is a confirmation nobody can give.

What the stack says beside the material, it says to every surface at once, so it is in words each of them can show (N1-R76). A terminal says how to replace the certificate as the command that does it, and any other surface in its own way. A sentence naming the command would have the app telling the operator to type something into a phone that has nowhere to type it.

Whatever the route, the app exchanges the credential once at /api/session for a session, and carries that session in X-Lemonfiber-Token afterwards. The credential is not kept to be re-sent: a secret held for one exchange is a smaller secret than one held for every request.

Re-pairing is the same machine, said again

The app asks an operator to pair again often enough that it is a routine rather than an edge: a certificate the stack announced it would replace (C6-R19), an address DHCP moved, retained state that could not be carried forward (N1-R34). Each time, what comes back has to land on the machine already held.

Nothing in the material says which machine that is. The address moves — that is one of the reasons for re-pairing. The fingerprint changes — that is another. Matching on either identifies a stack by the very thing that changed, and gets it wrong in exactly the cases the app itself asked for. N1-R22 already refuses that inference for pinning; it is no better as an answer to which of my machines is this.

So the material names the stack, with the stack’s own identifier, and the app matches on that. The alternative is a second row for a machine the operator paired once — the app disagreeing with itself about how many machines are in the house, on a screen whose entire job is to say.

The SDK is the only way out, and a gap in it is a question rather than a workaround

Every call to the stack goes through sdk-php. The app issues no HTTP request of its own, builds no URL, and parses no envelope the SDK did not hand it.

Where the SDK does not yet expose something the app needs, that is where the work stops. The gap is raised against the SDK and the contract, and the app waits. It does not reach past the SDK to the endpoint, re-implement the call beside it, or approximate the answer from something adjacent.

This is the rule that keeps a fourth client from quietly becoming a third source of truth. The failure it prevents is the ordinary one: a screen needs a field nobody generated yet, somebody fetches it directly “just this once”, and the contract now has a consumer it does not know about. A missing capability is information — it says the contract or the SDK is behind — and information is worth more than the screen it was blocking.

The first run is a route through, not a screen to work out

N1-R35 says a launch with no stack configured offers pairing rather than an empty operator surface. That is the floor, and a single screen with two buttons on it meets the floor while leaving the operator to work out what this application is, what it needs of them, and which of the two ways to pair is the one they can actually do right now.

Setup happens at the machine, and the app has to say so before it asks for anything. N1-R4 puts first-run setup on the host, and somebody who installed the app first has arrived in the wrong place — they need to be told that, once, in a sentence, rather than discovering it by pairing and finding nothing to do.

So the first run is a sequence with a stated end: what this is, what has to be true at the machine, and then the pairing itself. Each step says which one it is and how many there are, because a sequence whose length is unknown reads as a form that might not end. It is leavable at every step, and leaving lands on the pairing screen rather than on nothing — the operator who already knows all of this should not be made to read it.

Two people arrive at the first screen. Somebody who runs the server is there to pair it, and somebody who was invited is there to watch. The first screen asks which, in words that name no part of the system, and only the operator’s door leads to the sequence above (N1-R35). The invited door is the household’s own short way in (N3-R26), and an invitation opened before any house is on the phone goes straight to it.

It happens once and is not a setting. The sequence is shown while the device holds no pairing, which is the same condition N4-R22 reads for the lock, and it stops being shown the moment one is held. Neither an operator nor a screen can ask for it again: a wizard that can be re-entered is a place to get stuck, and everything it says is available afterwards in the surfaces that own it.

An unreachable stack is a state, not a missing feature

The app never hides an action because it cannot currently reach the stack. It offers the action and reports, in the words of G4, that the stack could not be reached and what to check.

This is the honest form of parity across a device boundary: the surface offers everything, and reachability is a condition it reports rather than a capability it lacks.

A frame reads once, and again only when it declared it would

A screen paints before it reaches the stack (N1-R25), and what it paints is built from values the frame goes on to read many times — a template asks for a count, then for the same count in a sentence beside it, then for the list the count came from. Each of those is a read of what the screen holds, and none of them is a reason to reach the machine a second time.

The failure this rules out is quiet, because it looks like nothing at all on the machine that suffers it. A screen that reaches the stack per value read, or per character typed into a filter, works perfectly in a test and on a desk. It is on the operator’s phone, over their own network, against a machine that is also serving the film somebody is watching, that one frame becomes thirty requests — and the cost lands on the thing the app exists to keep an eye on.

So the reading is an act the screen performs, once, and everything the frame asks afterwards is answered from what came back. Beyond that, a screen reaches a stack on the cadence it declares (N1-R27) or when the operator did something — never because a value was read, a key was pressed, or a screen was rebuilt (N1-R38).

A screen may hold the stream instead of reading

Some of what the core says is said only on its event stream. The health summary is the one that matters most: the core computes it once, into the dashboard, and publishes the dashboard nowhere else, so the one line every surface must say identically (G7) is on the stream or it is nowhere (ADR-0026).

A screen showing it holds a subscription rather than reading, and the subscription is that screen’s one read (N1-R67). Opening it is a call like any other, bounded like any other (N1-R68). Once it is open, what the core sends arrives on the core’s cadence, to every listener at once, and the screen takes what has already arrived on a cadence it declares. Taking it does not reach the stack, because nothing is sent (N1-R69).

A stream can go quiet without failing, so silence has a bound, and the bound is the contract’s: the core breaks silence with a heartbeat, and twice its interval with nothing heard is a broken stream (ARCH-R61). From then on the last value is a stale one, and a summary that was healthy is not healthy any more; it is unknown (N1-R70). A broken subscription is reopened on a declared cadence (N1-R71).

A subscription is held only while somebody is looking at what it carries. A phone in a pocket holding one open would wake its radio on every beat, all night, for a screen nobody can see (N1-R72).

Nothing is presented as current that is not

A value read four hours ago, shown without saying so, is the failure this product’s own dashboard rules exist to prevent. Anything the app shows from a previous connection is marked with when it was read, and is never mixed indistinguishably with a live reading.

A screen that refreshes on its own does not tell the operator how often. The cadence is declared by the screen, where a test holds it, and not printed: a line saying how often a screen looks is machinery, and it says nothing about whether what is shown is current. What does say that is the age a reading carries once it is no longer current (N1-R9), so that is what the operator is shown.

First-run setup does not cross the boundary

G1-R14 asks that setup be completable from every surface. It cannot be completed from a phone, and the reason is structural rather than a shortfall of effort: the thing that lets a phone reach the machine — a bound, authenticated admin surface — is created during setup. A phone cannot perform the act that makes a phone able to perform acts.

Reconfiguration (A4) is offered in full. First-run setup is declined with that reason stated, rather than absent.

More than one stack

An operator may run more than one. The app holds several, each with its own address and session, and never lets a reading from one appear under another. Switching is explicit.

Honest about the transport it got

C6’s deliberate position is that a LAN stack serves plain HTTP unless the operator has arranged otherwise, and that self-signed TLS is not on by default because it trains people to click through certificate warnings.

The app is the one client that cannot take that offer. A pin is compared against a certificate while the connection is being set up (ARCH-R99), and a plain HTTP address presents no certificate, so there is nothing for the pin to decide and ARCH-R60 refuses the address. Pairing a phone therefore requires TLS on the stack, and that is a cost stated here rather than discovered at the first pairing.

The warning-fatigue reasoning C6-R7 rests on does not apply to this surface. It is about a browser offering a human the choice to proceed. The app offers no such choice: it refuses, in software, against a value it was given out of band (ADR-0018). Self-signed is exactly what it expects, because no public authority will sign the name a stack is reachable under.

So what the app reports under N1-R12 is an encrypted connection whose certificate it pinned — not the platform trust store’s opinion of it, which it does not consult.

When remote access (I1) lands, the same session travels the overlay instead. The app’s conversation does not change.

States

State Meaning
Unpaired No stack is configured. The app explains what has to be true and how to make it so.
Paired, unreachable A stack is configured and cannot be reached now. Last-read values are shown, marked as such.
Paired, reachable A live session. Everything is current.
Subscribed, quiet A held subscription has heard nothing, not even the heartbeat, for twice the heartbeat interval. What it last carried is shown as stale, and a summary reads unknown.
Session expired The stack is reachable and the session is no longer valid. Re-authentication is offered without re-pairing.
Refused The stack answered and declined the credential. Distinct from unreachable, because the remedy is different.

Edge cases

Situation Behaviour
The operator scans a code for a stack the phone cannot route to Paired, then immediately unreachable, with the address named — a wrong network is the common cause and is worth naming.
The stack’s address changes with DHCP Reported as unreachable at the address held. Re-pairing is offered; the app does not scan the network for a replacement.
The operator pairs again with a machine the app already holds The machine already held is updated — its address, its pinned fingerprint, its name — rather than joined by a second row for the same stack.
Two stacks on one phone answer at the same address Each is held separately by what it calls itself, and a reading is never attributed to the wrong one.
The credential is correct and the surface is loopback-bound The stack is unreachable rather than refusing, and the app names the binding as the likely cause.
The device is offline entirely Unreachable, distinguished from a stack that is down, because the person can tell the difference and the remedy differs.
The operator switches away from the app with a subscription open Closed, and not reopened until the app is in front again with a screen that shows what it carries.
The stack stops sending, and the connection stays open Broken after twice the heartbeat interval in silence, not after the operating system gives up on the socket.
A subscription is reopened after a break What was held from before the break stays stale until the stream’s first value since replaces it.
The API answers a wire version the app does not know Refused loudly, naming both versions. An app that guessed at a newer envelope would render a stale shape as current.

Acceptance criteria

ID Requirement
N1-R1 The app MUST speak the published web API contract and MUST NOT implement a second API client of its own (ADR-0013).
N1-R2 Every action available from another surface MUST be offered by the app, except where a requirement here states otherwise and why.
N1-R3 The app MUST NOT hide or remove an action because the stack is currently unreachable; it MUST offer the action and report the reachability failure.
N1-R4 The app MUST NOT offer first-run setup, and MUST state that setup is performed at the machine and why, rather than omitting it silently.
N1-R5 The app MUST offer reconfiguration in full once connected.
N1-R6 Pairing MUST be possible by scanning a code, and MUST remain possible by typed entry where no camera is available or permission is declined.
N1-R7 The credential MUST be exchanged for a session once, and MUST NOT be retained for re-sending on subsequent requests.
N1-R8 The session MUST be carried in the credential header the API defines, and MUST NOT be placed in a URL or a query parameter.
N1-R9 Any value shown that was not read in the current session MUST be marked with when it was read, and MUST NOT be presented indistinguishably from a live reading.
N1-R10 A stack that cannot be reached, one that refuses the credential, and a device with no network MUST be reported as three different things, each with its own remedy.
N1-R11 The app MUST support more than one configured stack, MUST keep each one’s session separate, and MUST NOT attribute a reading from one stack to another.
N1-R12 The app MUST state whether the connection it has is encrypted, and MUST NOT imply protection it does not have.
N1-R13 The app MUST refuse an envelope whose wire version it does not support, naming the version it received and the versions it reads.
N1-R14 A change of transport — LAN today, an overlay under I1 later — MUST NOT require a change to how the app authenticates or to the contract it speaks.
N1-R15 The app MUST NOT log, transmit or include in a diagnostic report any credential, session token, or stack address.
N1-R16 Every call to the API MUST be made through the SDK. The app MUST NOT issue an HTTP request of its own, construct an API URL, or parse an envelope the SDK did not produce.
N1-R17 Where the SDK does not expose a capability the app requires, the app MUST NOT reach past it, re-implement the call, or approximate the answer from another endpoint. The gap MUST be raised against the SDK and the contract, and the dependent work MUST stop until it is closed.
N1-R18 The pairing material MUST carry the fingerprint of the certificate the stack will present, expressed as SHA-256 over that certificate’s DER encoding in lower-case hexadecimal, and the app MUST take it from that material rather than from the network (ADR-0018, ADR-0025). The digest of the certificate’s public key MUST NOT be carried in its place: it is the same length and a different value, so nothing but this sentence distinguishes them.
N1-R19 The app MUST pin the paired fingerprint against that stack and MUST validate every subsequent connection against it, whether or not the platform trust store would accept the certificate.
N1-R20 A connection presenting a different certificate MUST be refused rather than warned about, MUST be reported as this machine not being the one the app was introduced to, and MUST offer re-pairing as the remedy.
N1-R21 Certificate verification MUST NOT be disabled in any build, under any flag or configuration value.
N1-R22 Pinning MUST be to the stack rather than to an address, so that reaching the same stack by another route does not re-open the question of its identity.
N1-R23 A credential, a session token and pairing material MUST NOT be written to any cache, and MUST NOT survive beyond the storage N4 defines for them.
N1-R24 A reading the app retained from an earlier session MAY be shown on opening, and MUST carry when it was read as N1-R9 requires; the confirmation of an action just performed MUST NOT be served from a retained value.
N1-R25 A screen that reads from a stack MUST publish its first frame before issuing the read, and that frame MUST be built from what the app already holds (ADR-0019).
N1-R26 Every call the app makes MUST carry a bounded timeout short enough that a refused reach returns the screen to the operator, and the bound MUST NOT be raised to accommodate a slow stack.
N1-R27 A screen whose content can change while it is open MUST refresh on a cadence the screen declares, where a test holds it, and MUST NOT rely on the operator leaving and returning to see a change. The cadence MUST NOT be shown to the operator; what shows whether a value is current is the age N1-R9 requires.
N1-R28 An indeterminate progress indicator MUST be shown only where the app holds nothing to show, and MUST NOT replace a retained reading that could be shown with its age.
N1-R29 The app MUST establish, before offering an action, whether the connected stack supports it by reading the capability set the API declares (ARCH-R78), and MUST NOT determine this by attempting the action or by deriving it from a version number.
N1-R30 An action the connected stack is too old to offer MUST be reported as unavailable on that stack, naming what would provide it, and MUST NOT be hidden (N1-R3) or presented as a failure of the operator’s request. A capability that is present but unconfigured, and one the credential may not use, MUST each be reported as themselves rather than as absence (ARCH-R79).
N1-R31 Where two configured stacks differ in what they support, the app MUST NOT present the capabilities of one as the capabilities of another.
N1-R32 Anything the app retains between launches MUST carry the version of the shape it was written in.
N1-R33 On reading retained state written in an older shape, the app MUST either migrate it or discard it, and MUST NOT interpret it as though it were current.
N1-R34 Discarding retained state MUST NOT discard a pairing or its pinned fingerprint; where those cannot be carried forward, the app MUST say that re-pairing is required and why.
N1-R35 On a launch with no stack configured, the app MUST first ask which of two people is holding it, somebody invited and somebody who runs a lemonfiber server, in words that name no part of the system; the second MUST be told that setup happens at the machine (N1-R4) and offered pairing, the first MUST be given the member’s way in (N3-R26), and neither MUST be shown an empty operator surface.
N1-R36 On a launch with a stack configured, the app MUST reach a usable frame without waiting for a reading to complete.
N1-R37 A launch where the device has no network, where the stack cannot be reached, and where the app is locked MUST each be told apart, consistent with N1-R10 and the lock N4 defines.
N1-R38 Returning to a previous screen MUST restore what the operator had done there, and MUST NOT re-read the stack solely to rebuild it.
N1-R39 Which stack a screen is showing MUST be carried explicitly by that screen, and MUST NOT be read from state shared across screens.
N1-R40 An action the app could not deliver MUST be refused rather than retained, and the refusal MUST name the stack and state that nothing was changed (ADR-0020).
N1-R41 The app MUST NOT retain an undelivered action, MUST NOT replay one on reconnecting, and MUST NOT present an action as pending.
N1-R42 An idempotency key MUST accompany every action that changes a stack, and serves retry within a single attempt; it MUST NOT be used to replay an action across a reconnection.
N1-R43 A refused attempt MUST leave the action offered, consistent with N1-R3; the attempt failed, the capability did not become unavailable.
N1-R44 Where a session is rejected or has ended, the app MUST report it on the screen the operator is on, MUST offer to establish a new session there, and MUST restore that screen once one is established.
N1-R45 The end of a session MUST NOT discard the pairing or its pinned fingerprint; a credential expiring is not the machine changing (ADR-0018).
N1-R46 A session that has ended MUST be reported differently from a credential that was refused, consistent with N1-R10.
N1-R47 The stack MUST be able to produce pairing material on demand, from a surface the operator has already been admitted to, and MUST NOT require the app to be reachable in order to produce it.
N1-R48 Pairing material MUST carry the address the app should reach and the fingerprint of the certificate that address will present, and MUST NOT carry a credential.
N1-R49 Pairing material MUST expire, and redeeming it MUST NOT be what admits the app; admission remains the exchange of the operator’s own credential for a session (N1-R7).
N1-R50 Where pairing is completed by typed entry rather than by scanning (N1-R6), the app MUST present the fingerprint it observed in a form a person can compare against what the stack displays, and MUST NOT proceed on an unconfirmed fingerprint.
N1-R51 The comparable form MUST be short enough to be checked at a glance and MUST be derived from the whole fingerprint, so that two different certificates do not share one.
N1-R52 The application’s identity MUST be declared once in the repository, MUST be the same for every build of a given release, and MUST NOT be derived from the environment of whoever built it.
N1-R53 A build whose configured identity differs from the declared one MUST be refused, naming the declared identity and the configured one.
N1-R54 A launch with no stack configured MUST present a sequence that says what the app is, that setup happens at the machine (N1-R4), and then offers pairing — rather than a single screen the operator is left to interpret.
N1-R55 Every step of that sequence MUST say which step it is and how many there are, and MUST be leavable, with leaving landing on pairing rather than on nothing.
N1-R56 The sequence MUST be shown only while the device holds no pairing, MUST NOT be re-enterable once one is held, and MUST NOT be offered as a setting.
N1-R57 It MUST be possible to reach and operate every screen of the app with no stack on the network, by standing in for the stack itself rather than for any part of the app.
N1-R58 A stand-in MUST sit below the transport the app’s client uses, so that the client, the envelope reading and the wire-version check (N1-R13) exercised against it are the ones exercised against a real stack.
N1-R59 What a stand-in answers with MUST be derived from the published contract rather than written by hand, and a divergence between the two MUST fail a gate.
N1-R60 A stand-in MUST hold no credential, session or pairing material that could reach a real stack, and MUST NOT write any to the device’s store.
N1-R61 A released build MUST NOT be able to run against a stand-in, and that MUST be structural rather than a setting the app reads.
N1-R62 Pairing material MUST carry an identifier for the stack that is the stack’s own and is stable across re-issue of the material, a change of address, and a replacement of the certificate. It MUST NOT be derived from the address, from the fingerprint, or from any credential, and MUST NOT carry anything that identifies the household or an operator.
N1-R63 The app MUST decide which machine pairing material refers to from that identifier alone, and MUST NOT infer it from the address or the fingerprint. Material carrying an identifier the device already holds MUST replace what is held for that stack — its address, its pinned fingerprint and its name — rather than add a second.
N1-R64 A re-pairing that changes the pinned fingerprint MUST discard the session held for that stack; a re-pairing that does not MUST keep it, consistent with N1-R45. A different certificate is a different key, and a session obtained under the old one is not carried across it.
N1-R65 A screen MUST perform at most one read of a stack per frame it publishes, however many values that frame goes on to read from what came back. A value read more than once MUST be answered from what the screen holds rather than by reaching the stack again.
N1-R66 Beyond that read, a screen MUST reach a stack only on the cadence it declares (N1-R27) or in answer to an act of the operator’s. It MUST NOT reach one because a value was read, a key was pressed, or a screen was rebuilt (N1-R38).
N1-R67 A screen MAY hold a subscription to the stack’s event stream in place of reading, and opening it MUST count as that screen’s one read (N1-R65). Each value the stream delivers MUST be rendered from what the subscription holds, and MUST NOT be a reason to reach the stack again (ADR-0026).
N1-R68 A screen MUST publish its first frame before opening a subscription (N1-R25), and opening it MUST carry the bounded timeout every call carries (N1-R26). Once the subscription is open, that timeout MUST NOT bound how long it is held.
N1-R69 A screen MUST take what a subscription has delivered on a cadence it declares (N1-R27), and taking it MUST NOT wait for a value that has not arrived.
N1-R70 A subscription that has delivered nothing, neither a value nor the heartbeat, for twice the heartbeat interval the contract states (ARCH-R61) MUST be treated as broken. From then on its last value MUST be shown as stale with when it was read (N1-R9), and a summary it carried MUST read unknown rather than healthy.
N1-R71 A subscription that broke or could not be opened MUST be reopened on a cadence the screen declares (N1-R27), and MUST NOT be reopened more often than that. Nothing held from before the break MUST be presented as current until the reopened subscription delivers a value.
N1-R72 A subscription MUST be held only while a screen showing what it carries is in front of the operator. It MUST be closed when that screen is left, and at the screen’s first wake after the app leaves the foreground; it MUST NOT be opened or read while the app is in the background.
N1-R73 The comparable form (N1-R50, N1-R51) MUST be derived the same way wherever it is shown: SHA-256 over the fingerprint written as its sixty-four lower-case hexadecimal characters; the first sixteen bytes of that digest, each taken modulo thirty-two as an index into the alphabet 23456789ABCDEFGHJKLMNPQRSTUVWXYZ; written as four groups of four joined by hyphens. The fingerprint of sixty-four 0 characters MUST give 22VK-KPHH-NKH9-TUWA, and that of sixty-four f characters Z9JL-Q3PK-BZ6M-HRQZ.
N1-R74 Wherever the stack displays pairing material, it MUST display the comparable form of the fingerprint that material carries (N1-R73), in its human-readable and its machine-readable output alike, so that typed entry has something to be compared against (N1-R50).
N1-R75 The app MUST read a refusal whose code says the request carried nothing the stack admits as a session that has ended (N1-R44, ARCH-R138), and MUST NOT read a refusal with any other code as one.
N1-R76 What the stack says beside pairing material MUST be in words every surface can show, and MUST NOT name a command of one surface; how the certificate is replaced (C6-R19) MUST be said by each surface in its own way.
N1-R77 An answer the stack gave that this version of the app cannot read MUST be reported as neither a stack that did not answer nor a refused credential (N1-R10): the operator MUST be told the stack answered in a way this app cannot read, with updating the app or the stack as the remedy, and a member MUST be told the house cannot be reached right now.
N1-R78 A join link (D6-R22) MUST NOT be treated as pairing material: it MUST NOT admit an operator session or replace a pin, and the only credential it can lead to is the one member account it names.
  • G1 — the surfaces this joins, and the parity rule it inherits
  • C6 — the binding policy that decides whether a phone can reach anything
  • D6 — who is signing in
  • G4 — how a refusal is worded
  • I1 — the transport this is built to move onto
  • N2, N3, N4