Security. Defense in depth.
Plooi leans on Postgres RLS, per-row encryption, strict secret handling, and server-side validation. No single layer is the wall — every layer is.
What protects what
- Row Level Security (RLS) on every table —
auth.uid() = user_idenforced onSELECT,INSERT,UPDATE,DELETE, with aWITH CHECKon every mutation. - BYOK key encryption — AES-256-GCM with a per-row salt and nonce, key material derived per-row via HKDF-SHA256 from a master key kept in Supabase Function secrets (a separate store from Postgres).
- Bcrypt hashing for public-link passwords.
- Security headers — CSP, HSTS, X-Frame-Options, referrer policy.
- Stripe webhook signing and atomic idempotency at the database layer.
- Audit logs for every admin action, key save/delete, and deletion request.
- Granular admin roles scoped by capability rather than blanket access.
How BYOK keys are stored
- Cipher: AES-256-GCM, per-row 16-byte salt + 12-byte nonce.
- Key derivation: HKDF-SHA256 against a master key held in Supabase Function secrets — not in Postgres, not in env vars shipped to the browser.
- Writes: browser →
store-api-keyedge function → encrypt → store. Plaintext exists for one network hop. - Reads from the browser: only through the
user_api_keys_publicview, which projectsid,service_name,key_hint(last four chars), andkey_version. The ciphertext column isREVOKEd fromauthenticatedentirely. - Decrypts: only inside
ai-proxy, in memory, for one provider call. Plaintext is discarded on response. - Never written to application logs, audit logs, Sentry, analytics, or database backups.
Errors that bubble up from providers are sanitized server-side. Patterns
like sk-ant-*, sk-proj-*, AIzaSy*, ?key=*, and Bearer * are
stripped before they reach the browser, Sentry, or any log line.
See Bring your own key for the canonical version.
Secrets and keys
- Frontend variables only ever via
VITE_*— these end up in the browser, so they cannot hold anything sensitive. - Server-side secrets only via Supabase Secrets — Stripe webhook key, Service Role key, master encryption key, cron secrets.
- The Service Role key is never used in the frontend. If you find a reference, it’s a bug.
⚠️
Never commit secrets to git or .env.local. Anything with admin
privileges lives in Supabase Secrets.
Ongoing practice
- MFA for admins (planned).
- Dependabot for dependency updates.
- Monitoring on unusual auth events and on the rate of
store-api-key/delete-api-keycalls per user.