# 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
MEMBERSHIP_ENCRYPTION_KEY=<32+ random bytes>
ACCESS_HMAC_KEY=<32+ random bytes>
ACCESS_CONTROLLER_TOKEN=<random controller token>
HOME_ASSISTANT_UNLOCK_WEBHOOK_URL=<local HA webhook URL, optional>
HOME_ASSISTANT_UNLOCK_TIMEOUT_MS=2500
BTCPAY_SERVER_URL=https://your-btcpay-host
BTCPAY_STORE_ID=<store id>
BTCPAY_API_KEY=<api key>
BTCPAY_WEBHOOK_SECRET=<webhook secret>
DEV_SEED_MEMBERS=false
```

Keep `server/data` on a persistent volume. Do not deploy `.env.local`.

## 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: <ACCESS_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.
