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
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:
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>
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:
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 controller-facing endpoint is:
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:
{
"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.
Security Notes
- The generated user
nsecis 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.
- Controller APIs require
ACCESS_CONTROLLER_TOKEN. - Mutating API routes have basic rate limiting.
- Rotate any development BTCPay credentials before production.