Encrypted Cache
How to use
Install
yarn add @mongez/cache @mongez/encryption@mongez/encryption (^2.0.0) is a peer dependency needed only for the encrypted drivers. You may supply your own encrypt/decrypt pair (sync or async) if you prefer.
Full setup with @mongez/encryption
import { encrypt, decrypt, setEncryptionConfigurations,} from "@mongez/encryption";
import { EncryptedLocalStorageDriver, setCacheConfigurations,} from "@mongez/cache";
// 1. Configure the encryption key once at boot.setEncryptionConfigurations({ key: "your-app-secret" });
// 2. Pass the driver and the encrypt/decrypt pair to the cache.setCacheConfigurations({ driver: new EncryptedLocalStorageDriver(), encryption: { encrypt, decrypt, },});From this point the cache API is identical to the plain drivers, apart from await:
import cache from "@mongez/cache";
await cache.set("token", "abc123");// localStorage: { "token": "<AES-GCM cyphertext>" }
await cache.get("token"); // "abc123" ← decrypted transparentlyEncrypted sessionStorage variant
Replace the driver; everything else is identical:
import { EncryptedSessionStorageDriver, setCacheConfigurations } from "@mongez/cache";import { encrypt, decrypt, setEncryptionConfigurations } from "@mongez/encryption";
setEncryptionConfigurations({ key: "your-app-secret" });
setCacheConfigurations({ driver: new EncryptedSessionStorageDriver(), encryption: { encrypt, decrypt },});Encrypted IndexedDB variant (opt-in)
For structured values or storage beyond the ~5MB Web Storage quota, EncryptedIndexedDBDriver encrypts the whole {data, expiresAt} envelope while keeping a plaintext expiresAt on the record so expired rows can be evicted without decrypting every entry on GC. See the indexeddb skill for driver options and error types.
import { EncryptedIndexedDBDriver, setCacheConfigurations } from "@mongez/cache";import { encrypt, decrypt, setEncryptionConfigurations } from "@mongez/encryption";
setEncryptionConfigurations({ key: "your-app-secret" });
setCacheConfigurations({ driver: new EncryptedIndexedDBDriver(), encryption: { encrypt, decrypt },});
await cache.set("auth.tokens", { access, refresh }, 60 * 30);Bringing your own encrypt/decrypt pair
Any object with encrypt(value: any): any | Promise<any> and decrypt(value: any): any | Promise<any> is valid. The pair is called (and awaited) on every set and get:
import { EncryptedLocalStorageDriver, setCacheConfigurations } from "@mongez/cache";import CryptoJS from "crypto-js";
const SECRET = "app-secret";
setCacheConfigurations({ driver: new EncryptedLocalStorageDriver(), encryption: { encrypt: (value) => CryptoJS.AES.encrypt(JSON.stringify(value), SECRET).toString(), decrypt: (value) => { const bytes = CryptoJS.AES.decrypt(value, SECRET); return JSON.parse(bytes.toString(CryptoJS.enc.Utf8)); }, },});TTL with encrypted drivers
The encrypted drivers wrap values in the same {data, expiresAt} envelope as the plain drivers before encrypting. TTL works identically:
await cache.set("token", "abc123", 60 * 15); // expires in 15 minutes
// On disk: encrypted({ data: "abc123", expiresAt: <timestamp> })// On get after expiry: entry is removed, defaultValue is returned.Set a global default in setCacheConfigurations:
setCacheConfigurations({ driver: new EncryptedLocalStorageDriver(), encryption: { encrypt, decrypt }, expiresAfter: 60 * 60, // 1-hour default});Key prefixing
Works the same as plain drivers:
setCacheConfigurations({ driver: new EncryptedLocalStorageDriver(), encryption: { encrypt, decrypt }, prefix: "secure-",});
await cache.set("user", { id: 1 });// localStorage key: "secure-user", value: ciphertextawait cache.get("user"); // { id: 1 }Rotating the encryption key
Because the encrypt/decrypt pair is read from configuration on every call, you can rotate the key without rebuilding driver instances:
import { setEncryptionConfigurations } from "@mongez/encryption";import { setCacheConfigurations, getCacheConfigurations } from "@mongez/cache";
// Rotate the @mongez/encryption key:setEncryptionConfigurations({ key: "new-secret" });
// Re-apply config to pick up the new functions:const existing = getCacheConfigurations();setCacheConfigurations({ ...existing, encryption: { encrypt, decrypt }, // encrypt/decrypt close over the new key} as any);Note: existing ciphertext written with the old key cannot be decrypted after rotation — it fails authentication and is evicted on the next read rather than thrown. Clear the cache or migrate entries before rotating in production.
Wiring to @mongez/atom with encrypted storage
Tokens and PII stored through @mongez/atom persist automatically using encrypted storage when you use an encrypted driver in the shared cache adapter:
import cache from "@mongez/cache";
export const secureAdapter = { get: (key: string) => cache.get(key), set: (key: string, value: unknown) => cache.set(key, value), remove: (key: string) => cache.remove(key),};import { createAtom } from "@mongez/atom";import { secureAdapter } from "./adapters/cacheAdapter";
const authAtom = createAtom({ key: "auth.token", default: null, persist: secureAdapter,});Because the adapter delegates to the cache which uses an encrypted driver, all atom values are transparently encrypted at rest.
Key details / Pitfalls
encryptionkey insetCacheConfigurationsis mandatory. Without it,getCacheConfig("encryption")returnsundefinedand the driver will throw a runtime error onsetorget. Always pass{ encrypt, decrypt }.- The
encryptionpair is called with the raw envelope object before JSON-stringification. The driver passes{ data: value, expiresAt }directly toencrypt. Yourencryptfunction receives a plain object, not a string, and may return a value or aPromise. clear()is scoped to the configured prefix; with no prefix, it wipes the entire backing store. Useremove(key)for targeted deletion, or set aprefixon shared domains.- Legacy ciphertext (pre-envelope format) is handled gracefully. If a stored ciphertext decrypts to something without
data/expiresAtkeys (i.e. written before the envelope was introduced), the driver returns the decrypted value as-is with no expiry check. This prevents data loss during upgrades. - A tampered or undecryptable entry is evicted, not thrown. If the ciphertext is corrupted or the key has been rotated,
get()removes the entry and resolves todefaultValuerather than throwing — a single poisoned row cannot make every later read fail. - Do not mix plain and encrypted drivers on the same key. Reading a plaintext envelope with an encrypted driver (or vice versa) resolves to
null/ default, not an error. Clear stale entries after switching drivers. EncryptedSessionStorageDriverhas tab-lifetime persistence. UseEncryptedLocalStorageDriverorEncryptedIndexedDBDriverwhen you need values to survive tab close and reopen.