access/README.md
Patrick Mulligan 40564f06ec feat: access extension — NFC door access via boltcards SUN + Home Assistant
Promoted from the door-portal scratch repo to its own repo for install via the
aiolabs catalog. Authenticates a tapped Bolt Card via boltcards /verify,
authorizes against per-door grants, and fires the door's local Home Assistant
webhook to unlock a Z-Wave lock. Fails closed; logs every attempt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 22:47:41 +02:00

2.8 KiB

Access — LNbits extension

NFC door access for LNbits. A tapped Bolt Card (NTAG424) is authenticated via the boltcards SUN check, authorized against per-door grants, and — if allowed — the door's local Home Assistant webhook is fired to unlock a Z-Wave lock. Every tap is logged.

Runs on the on-prem LNbits on the door's LAN, so the unlock path stays local and the door does not depend on the internet.

Flow

Pi + PN532 reader
  → POST /access/api/v1/check   (X-Controller-Token: <door token>)
        { "doorId": "...", "external_id": "...", "p": "...", "c": "..." }
        1. authenticate  → boltcards /verify  (valid, non-replayed SUN?)
        2. authorize     → active grant for this external_id on this door?
        3. actuate       → POST door.ha_webhook_url  (→ lock.unlock → Z-Wave)
        4. log           → access.logs (allow/deny + reason)
  → { "allow": true|false, "reason": "..." }

The reader never unlocks directly; this extension owns the allow/deny decision and the Home Assistant call. Fails closed at every step.

Data model

  • doors — a resource: name, controller_token (reader secret), ha_webhook_url (local HA webhook), boltcards_base_url (blank = this instance), unlock_timeout_ms, enabled.
  • grantsexternal_id → door permission, enabled/expires_at.
  • logs — every allow/deny with a reason.

Admin API (wallet admin key)

  • GET/POST /access/api/v1/doors, PUT/DELETE /access/api/v1/doors/{id}
  • GET/POST /access/api/v1/grants, DELETE /access/api/v1/grants/{id}
  • GET /access/api/v1/logs

Reader API (controller token)

POST /access/api/v1/check
X-Controller-Token: <door.controller_token>
Content-Type: application/json

{ "doorId": "front-door", "external_id": "abc123", "p": "<32-hex>", "c": "<16-hex>" }

Setup

  1. Install on the on-prem LNbits (same instance as boltcards).
  2. Create a door, set its Home Assistant webhook URL, copy its controller token to the reader config.
  3. Issue Bolt Cards in boltcards as usual; add a grant (the card's external_id → the door).
  4. Point the door reader (Pi) at /access/api/v1/check with the token.

Notes

  • Card authentication delegates to boltcards over loopback HTTP via its /api/v1/verify/{external_id} endpoint (aiolabs fork ≥ 1.1.1-aio.2) — a side-effect-light SUN check that returns {"authenticated": true, …} without /scan's spend semantics (no withdrawRequest, no daily-limit, no "hit"), while still advancing the SUN counter for replay protection.
  • Requires boltcards 1.1.1-aio.2+ installed on the same LNbits.
  • Empty ha_webhook_url = authorize + log only (bring-up mode; no unlock).
  • Keep this on the LAN; use a strong per-door controller token; never expose Home Assistant publicly.