Encrypted Storage (AES-256-GCM) Locked

Records are encrypted with AES-256-GCM the moment they are written, and decrypted only when you read them back. Your passphrase derives an in-memory key; locking drops that key but touches nothing on disk.

How it works

flowchart LR
    P[Passphrase] -- derive once --> K((In-memory key))
    K -. drop .-> LK[Lock]
    W["db.create(record)"] --> E[encrypt one record] --> I[(IndexedDB
ciphertext)] I --> D[decrypt one record] --> R["db.read(id)"] E -. uses .-> K D -. uses .-> K style K fill:#ffe082,stroke:#f57f17 style I fill:#ede7f6,stroke:#512da8 style LK fill:#ffcdd2,stroke:#c62828

Try it — a five-step tour

  1. Click Unlock above (default passphrase test123). This only sets up the key — nothing gets decrypted yet.
  2. Click Seed sample records. Each db.create encrypts its record and writes the ciphertext to IndexedDB.
  3. The Records panel now shows those records decrypted on demand — it called db.list('users'), which decrypted each row individually.
  4. Click Lock. The in-memory key is dropped. The same rows are still in IndexedDB — but the Records panel now reads them raw and shows the ciphertext (12-byte nonce + encrypted JSON). No bulk re-encryption happened; the bytes on disk are unchanged.
  5. Click Unlock again to flip back to decrypted columns. Type a wrong passphrase first to see invalid passphrase.

Data Operations

🔓 Records

Unlocked view: db.list('users') returns the plaintext fields.

No data yet — unlock and seed records.

🗄 All raw storage keys

Every key in IndexedDB — records plus metadata (schema, constraints, salt, check blob). Keys are plaintext, values are AES-GCM ciphertext.

Nothing to show yet — unlock and add records.

Advanced: persistent metadata

Schemas, constraints, indexes, and relationships persist to encrypted storage just like records. These buttons exercise each async metadata API.

Log