The core philosophy of this application relies on a strict separation between authentication (identity) and decryption (access). The server acts solely as a gatekeeper and never as the pipe.
Internalizing that these layers are entirely independent is what stops insecure design. The server can confirm Layer 1 all day, but it has no way to ever observe Layer 2.
Handled by Clerk. This dictates if the user can make HTTP requests to your server. The server verifies the JWT and knows the user's identity, plan tier, and account limits.
Handled locally in the browser. The Vault Key exists purely in volatile memory, derived from the Master Password. A logged-in Clerk user with a locked vault can do nothing with their data.
Because a Clerk user exists before the vault does, the application gates access based on the vault_initialized database flag.
Exactly what the Postgres database and your Node/Next.js backend can and cannot observe.
How the cryptographic primitives map to user actions.
Browser generates a 256-bit Vault Key. Derives KEK from Master Password (Argon2id + salt). Generates BIP39 recovery phrase. Wraps Vault Key twice. Sends only wrapped blobs + salt to server.
Fetch wrapped_vault_key + salt. Re-derive KEK from typed password. Unwrap key locally. AES-GCM auth tag ensures wrong passwords simply fail. No hashes stored or compared server-side.
Encrypt full entry as JSON {title, username, pass...} client-side. Server checks item_limit against plan, then atomically inserts row + bumps counter in one transaction.
Server returns raw ciphertext rows. Browser decrypts them into memory. Search runs completely client-side using Fuse.js over the decrypted objects.
Enter 12 words → derive recovery key → unwrap Vault Key via recovery_wrapped_key → immediately set new master password (re-wraps Vault Key). Entries remain untouched.
Clerk user.deleted webhook deletes the users row. Postgres ON DELETE CASCADE handles wiping folders, items, and logs instantly. No orphaned external data.