This is a SHORT document, explaining the security measures in place. It's not the place for AI slop, or legaleeze.
There are two auth flows: one for the admin dashboard, one for devices (Electron app).
A passphrase-based system. The passphrase is set via DASHBOARD_SECRET env var on the server.
- User enters passphrase on login page
- Server compares it to
DASHBOARD_SECRET - If valid, sets an httpOnly cookie (
context_admin) containing the passphrase - Cookie settings: httpOnly, secure in production, sameSite=lax, 1 year expiry
- Subsequent requests check if cookie value matches
DASHBOARD_SECRET
In development, if DASHBOARD_SECRET is unset, auth is bypassed entirely.
Devices authenticate via a shared secret:
- Set
API_WRITE_SECRETenv var on the server - Enter the same secret in the desktop app settings
- Desktop app sends it as
Authorization: Bearer <secret>header - Server rejects requests where the token doesn't match
If API_WRITE_SECRET is unset on the server, device auth is bypassed (for development).
API read is actually more sensitive than write.
Messages and screenshots are encrypted on the desktop before upload. The encryption key never leaves your browser.
- Desktop app encrypts data locally before sending to the server
- Server stores encrypted blobs (it cannot read your data)
- Web dashboard downloads encrypted data and decrypts it locally
- The encryption key is stored in browser
sessionStorage(must not be sent anywhere) - Decryption uses the Web Crypto API (
crypto.subtle) in your browser
The server has no way to decrypt your data. If you lose your encryption key, your data is unrecoverable.
What we do now:
- Single encryption key per user (passphrase-derived via PBKDF2)
- Encrypted values are prefixed with
enc:v1:to self-identify as encrypted - Some fields are encrypted (message text, lat/long), others are plaintext (timestamps, contact IDs) so the server can index/query
- No key identifier stored — we assume all data uses the same key
- The
v1inenc:v1:indicates the encryption format version, not which key was used
What we could do (but don't):
- Key rotation with key IDs: Format could become
enc:v1:key01:iv:tag:ciphertextto track which key encrypted what. Would allow rotating keys without re-encrypting old data. - Per-table or per-field keys: Different keys for messages vs locations. More complexity, unclear benefit.
- Key escrow / recovery: Store an encrypted backup of the key somewhere. Defeats zero-knowledge if done wrong.
Trade-offs of zero-knowledge encryption:
- Forget your key → data is permanently lost (feature, not a bug)
- Change your key → must re-encrypt everything
- Multiple devices → all need the same passphrase
- Server can't help you recover anything
Open questions Felipe doesn't know the answer to:
- Is PBKDF2 with 100k iterations still considered good? (Argon2 is "better" but more complex)
- Should we ever implement key rotation, or is "re-encrypt everything" fine for a personal tool?
- Do we need to encrypt more fields? (e.g., contact phone numbers are currently plaintext for querying)
Configure rate limiting in the Vercel dashboard under Firewall → + New Rule.
Recommended rule for API endpoints:
- Name:
api-rate-limit - If: Request Path starts with
/api - Rate Limit: Fixed Window, 300 seconds, 10 requests, Key: IP Address
- Then: Too Many Requests (429)
See docs/firewall-example.png for reference.