# L484 / Sapien Membership App Vue frontend with a small Node backend for private membership signup, Nostr sign-in, admin approvals, BTCPay invoices, encrypted local member-card export/import, and NFC card access scaffolding. ## Development ```bash npm install npm run dev ``` The dev script runs the API and Vite with `--host`. It also loads `.env.local` and seeds development members when `DEV_SEED_MEMBERS=true`. ## Production / Portainer The app listens on port `2354` in the provided compose files. Required production environment: ```bash PORT=2354 HOST=0.0.0.0 APP_MODE=all ADMIN_ALLOWED_HOSTS=admin.local,l484.local MEMBERSHIP_ENCRYPTION_KEY=<32+ random bytes> ACCESS_HMAC_KEY=<32+ random bytes> ACCESS_CONTROLLER_TOKEN= HOME_ASSISTANT_UNLOCK_WEBHOOK_URL= HOME_ASSISTANT_UNLOCK_TIMEOUT_MS=2500 BTCPAY_SERVER_URL=https://your-btcpay-host BTCPAY_STORE_ID= BTCPAY_API_KEY= BTCPAY_WEBHOOK_SECRET= DEV_SEED_MEMBERS=false ``` Keep `server/data` on a persistent volume. Do not deploy `.env.local`. The admin UI, admin APIs, and controller card-scan endpoint are available only when the request comes from localhost, a private LAN IP, a `.local` hostname, or a hostname listed in `ADMIN_ALLOWED_HOSTS`. Public members can still use `/api/member/door/unlock` from the external site when their local membership secret verifies an active paid membership. ## BTCPay Create a BTCPay webhook pointing at: ```text https://your-domain/api/btcpay/webhook ``` Use the same webhook secret as `BTCPAY_WEBHOOK_SECRET`. The admin payment modal opens a live server-sent event stream at `/api/admin/events`. When BTCPay calls the webhook, the backend marks the invoice paid and pushes a `payment-paid` event to open admin sessions. The status endpoint remains available for explicit refresh/error recovery. ## NFC Door Readiness The planned lock is a Kwikset Home Connect 918. It unlocks over Z-Wave, so the NFC reader does not directly control the lock. The current plan is: ```text PN532 -> ESP32 -> L484 backend allow/deny -> Home Assistant/Z-Wave JS -> Zooz ZST39 LR -> Kwikset 918 ``` Full hardware and wiring plan: [docs/nfc-door.md](docs/nfc-door.md). ESP32 firmware scaffold: [firmware/esp32-pn532-door](firmware/esp32-pn532-door). The controller-facing endpoint is: ```http POST /api/access/check X-Controller-Token: Content-Type: application/json { "doorId": "front-door", "cardCredential": "scanned-card-secret-or-uid" } ``` The backend stores only HMACs of card credentials using `ACCESS_HMAC_KEY`. Access is allowed only when: - the scanned card hash matches an active card, - the member exists, - the member access status resolves to `active`. The response: ```json { "allow": true, "reason": "active_member_card", "member": { "membershipId": "L484-2026-XXXXXX", "fullName": "Member Name", "status": "active" } } ``` Every access check is logged in `server/data/access-logs.json`. If `HOME_ASSISTANT_UNLOCK_WEBHOOK_URL` is configured, an approved controller request also calls that local webhook so Home Assistant/Z-Wave JS can unlock the Kwikset 918 through the Zooz stick. Admin mock scans do not trigger unlocks. ## Security Notes - The generated user `nsec` is shown once in the browser and is not sent to the backend. - Member records are encrypted before being saved in `server/data/memberships.json`. - NFC card credentials are never stored raw. - Admin APIs require an authorized Nostr public key. Set `MASTER_ADMIN_PUBKEY` to the master admin `npub`; only that key can approve or reject other admin keys. - Controller APIs require `ACCESS_CONTROLLER_TOKEN`. - Mutating API routes have basic rate limiting. - Rotate any development BTCPay credentials before production.