System Architecture

Zero-Knowledge Design

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.

The Dual Trust Boundary

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.

Layer 1: Clerk Session

"Is this person logged in?"

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.

Layer 2: Vault Unlock

"Is the vault decrypted right now?"

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.

User Lifecycle (State Machine)

Because a Clerk user exists before the vault does, the application gates access based on the vault_initialized database flag.

Anonymous
No Session
Logged In
vault_initialized = false
Setup Flow
Vault Locked
Key NOT in memory
UnlockAuto-Lock
Vault Unlocked
Vault Key in memory

Server Visibility Spec

Exactly what the Postgres database and your Node/Next.js backend can and cannot observe.

Server Can See

  • User Identity (via Clerk JWT)
  • Total item count (for plan limits)
  • When items were created/updated
  • Item type (Login, Note, Card)
  • Favorite flag status
  • Subscription plan tier

Server Cannot See

  • Item Titles or Usernames
  • Passwords or URLs
  • Secure Notes or Folder Names
  • The Master Password
  • The BIP39 Recovery Phrase
  • The Unwrapped Vault Key

Core Operational Flows

How the cryptographic primitives map to user actions.

1. Vault Initialization

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.

2. Unlock Mechanism

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.

3. Add / Edit Item

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.

4. List & Search

Server returns raw ciphertext rows. Browser decrypts them into memory. Search runs completely client-side using Fuse.js over the decrypted objects.

5. Forgot Master Password

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.

6. Account Deletion

Clerk user.deleted webhook deletes the users row. Postgres ON DELETE CASCADE handles wiping folders, items, and logs instantly. No orphaned external data.