Security
Zero-knowledge encryption
Plan data is encrypted on the client (AES-GCM). The encryption key is derived from the user's password (PBKDF2), and in a Duo plan it's shared between household members via key exchange (ECDH). The key never reaches the server in any form. We don't roll our own cryptography — we use the browser's native Web Crypto API.
The upshot: even with full access to the database, the data can't be read without the user's password or recovery code.
Access control (Row Level Security)
In cloud mode, data access is enforced by Postgres RLS: queries only see and
modify the household you're a member of. Operations such as
creating/joining/leaving a household or saving state go through security definer
functions that check permissions in a controlled way. RLS acts as an extra layer
(defense-in-depth) on top of the ciphertext — it limits who can even fetch a
record, which is unreadable without the key anyway.
Keys and secrets
- In the browser, only the public
anonkey is used, protected by RLS. - Sensitive keys (e.g. service-role, payment-provider keys) never reach the frontend — they live only on the server (environment variables).
- No secrets in the repository.
Data validation
Every form and every read of the state goes through Zod schemas — invalid or malicious data never reaches the app. Text is stripped of control characters.
Passwords
Passwords aren't stored in plaintext — Supabase keeps only a one-way hash (bcrypt). They can't be read; they can only be reset or overwritten.
Destructive actions
Resetting data and deleting your account require password confirmation to prevent accidental data loss.
Transport and headers
Communication over HTTPS, with security headers (including a CSP).