Sharing & QR labels

Give someone control of exactly one thing, for exactly as long as you mean to — and label your hardware so you can find it again.

A shared link is a tokenised public URL that operates a single peripheral, a limited number of times, within a validity window, optionally behind a PIN. The recipient needs no account, no app and no access to anything else in the house.

The typical case: a visitor arriving tomorrow who needs the gate to open three times.

Creating one

Share Access sits in the dropdown of every peripheral card that supports it.

Share access dialog
Share AccessOptional PIN, a description of who it is for, a start and expiry date, and a maximum number of uses.
FieldMeaning
PINOptional. Leave empty for a link that works on its own.
DescriptionWhy and for whom — it shows in the admin list, which is what you will be reading in three weeks.
Starts onBefore this the link reports not yet active.
Expires onAfter this it flips to expired automatically.
Max usesTotal allowed actions. For switches this reads max starts.

The result is a URL of the form https://your-host/shared/<32-hex-token>.

Widget types

TypePeripheralActions
GATE_ACCESSGate or door lockopen — a single pulse
LIGHTAny lighton, off
WATER_PUMPWater pumpon, off
SPRINKLERAny sprinkleron, off

How uses are counted

A gate link spends one allowance per open. A switch link spends one per startoff is always free, and stays available even after the allowance is spent or the end date has passed.

This asymmetry is deliberate.

Someone who has used their last start must still be able to stop the water. Only an explicit Disable or Archive removes the stop control.

AllowanceActionResultUsed
2onstarted1
2offstopped1 (unchanged)
2onstarted; link flips to EXPIRED2
2on403 — allowance spent2
2offstopped — still permitted2

What the guest sees

PIN entry for a gate link
PIN gateShown only when the link has one.
Gate unlock page
Gate controlTap to unlock, uses remaining, expiry.
PIN entry for a switch link
PIN switchSame flow, switch styling.
Switch control page
Switch controlStart / stop, remaining starts, auto-off hint.

The public page shows live state, how many starts remain, when the link expires and — for switches — the auto-off timeout inherited from the peripheral (“stops automatically after 5 hours”). It has no authenticated WebSocket, so it re-reads state after each action and offers a manual refresh.

States

StateMeaning
VALIDUsable.
NOT_YET_ACTIVEStart date is in the future. Computed, never stored.
EXPIREDEnd date passed or allowance exhausted. The transition is automatic and records its reason.
DISABLEDSwitched off by an admin. Reversible.
ARCHIVEDRetired. Kept for its audit history.

Audit trail

Every attempt is recorded — successful or denied — in a dedicated table, one row per attempt: the action, the result (SUCCESS, DENIED_PIN, DENIED_STATE), the reason for a denial, the client address and the user agent.

In the admin list, clicking a link's usage or history opens a collapsible usage history: timestamp, action, result badge, IP and user agent.

Behind a reverse proxy, set trustedProxies.

The audit records the first hop of X-Forwarded-For, falling back to the socket peer. Without a trusted-proxy list you will see your proxy's address on every row instead of the visitor's. See Configuration → Security settings.

Public API

These three endpoints are the only anonymous surface in the whole application.

MethodPathPurpose
GET/api/public/share/:tokenLink metadata. Never returns the PIN.
POST/api/public/share/:token/verify-pinCheck a PIN without acting.
POST/api/public/share/:token/actionPerform the action.
GET /api/public/share/<token>

{
  "widgetType": "WATER_PUMP",
  "requiresPin": false,
  "state": "VALID",
  "peripheralName": "Water pump",
  "actionsAllowed": 2,
  "actionsUsed": 0,
  "shareExpireDate": 1755500000000,
  "currentState": false,
  "autoOffTimeout": "900",
  "offAllowed": true
}
POST /api/public/share/<token>/action
{ "action": "on", "pin": "1234" }

{ "success": true, "action": "on", "actionsRemaining": 1, "offAllowed": true }

Failure modes: 400 for a missing token or an action other than on/off; 404 for an unknown token; 403 for a wrong PIN or an unusable state.

A success means the command was published, not confirmed.

The action is dispatched onto the event bus fire-and-forget. The public page re-reads metadata a moment later to show the device's real state.

Security properties, stated plainly


QR labels

The second half of “finding your own hardware again”: printable labels for cables, devices and peripherals, with an optional QR code, and an in-app scanner that jumps straight to the right screen.

Printing

The QR does not contain your hostname.

It encodes myhab://TYPE/ID, so a label printed today keeps working if you move the deployment to a different domain tomorrow. Resolution happens inside the app, which already knows where it is.

Scanning

Scan QR in the sidebar opens the device camera and navigates directly to the cable, device or peripheral on the label. It is pure web — the PWA using the camera API — so there is nothing to install.

The practical value is in the attic: point a phone at a cable tag and get the port it lands on, the peripheral it feeds, and its event history, without knowing anything about how it was named.