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.
db.create, db.update): MQDB encrypts that one record with the in-memory key and stores the ciphertext in IndexedDB.db.read, db.list): MQDB fetches that one record's ciphertext from IndexedDB and decrypts it on the fly.data/users/1) are plaintext so prefix scans work. Only values are ciphertext.
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
Unlock above (default passphrase test123). This only sets up the key — nothing gets decrypted yet.Seed sample records. Each db.create encrypts its record and writes the ciphertext to IndexedDB.db.list('users'), which decrypted each row individually.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.Unlock again to flip back to decrypted columns. Type a wrong passphrase first to see invalid passphrase.Unlocked view: db.list('users') returns the plaintext fields.
No data yet — unlock and seed records.
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.
Schemas, constraints, indexes, and relationships persist to encrypted storage just like records. These buttons exercise each async metadata API.