# Sensitive apps (the hardening kit) Source: https://selfstore.dev/docs/sensitive Configure selfstore for data whose leak is serious - health, legal, personal. Make encryption and a strong password non-optional, lock the local cache behind a secret, and offer only the destinations you allow. No fork required. selfstore's defaults are already private: no server, backups encrypted end to end, and the local IndexedDB cache is [sealed at rest](https://selfstore.dev/docs/security) under a per-device key. For an app where a leak is genuinely serious - health notes, a legal file, anything intimate - you can make those guarantees **non-optional** and add one more, all through options. No fork, no separate build. ## 1. Refuse plaintext: `requireEncryption` Turn encryption from a default into an invariant. The store then refuses every path that could leave a readable copy anywhere it does not control: ```ts const store = await selfstore('clinic-notes', { requireEncryption: true }); ``` - Connecting a destination without a password (or a group) throws `ENCRYPTION_REQUIRED` - a home may only receive ciphertext. - `unprotect()` / turning encryption off is refused. - Exporting a plaintext backup is refused. The on-device working cache is unchanged by this flag (it is governed by the browser profile, and by `cacheLock` below); `requireEncryption` is about the copy that **travels**. ## 2. Demand a strong password: `passwordPolicy` A password floor, enforced at the store - so no screen in your app can forget to check it: ```ts const store = await selfstore('clinic-notes', { requireEncryption: true, passwordPolicy: { minLength: 12, requireUppercase: true, requireDigit: true, requireSymbol: true, }, }); ``` A weaker password is rejected with `WEAK_PASSWORD` at `protect()` and at every key add. For a live UI hint as the user types, preview the same rules without mutating anything: ```ts import { checkPasswordPolicy } from 'selfstore'; const { ok, unmet } = checkPasswordPolicy(candidate, policy); // unmet: ('minLength' | 'lowercase' | 'uppercase' | 'digit' | 'symbol')[] ``` It is unicode-aware (an accented letter counts as a letter, an emoji as one character). A policy bounds the worst case; it cannot make a compliant password unguessable. Argon2id still does the [slow part](https://selfstore.dev/docs/format). ## 3. Lock the local cache: `cacheLock` At-rest cache encryption is always on, but its key sits next to the data, so it falls to a copy of the whole browser profile. For the top tier, seal the cache under a key held **in memory only** - a password, or a key your app already has (a passkey PRF result). A copied profile then carries no usable key. ```ts const store = await selfstore('clinic-notes', { requireEncryption: true, passwordPolicy, // Called once at boot to unlock the cache; re-called on a wrong secret. cacheLock: async ({ failed }) => promptForPassphrase({ failed }), }); ``` There is no way around one unlock per session: a secret that could be derived without asking could be derived by whoever copied the profile too. **Branch it on your app's existing login** and the UX does not change - you already had an unlock moment. See [cacheLock in the security guide](https://selfstore.dev/docs/security#cachelock) for the honest boundary (it beats a profile copy, not code running in your origin). ## 4. Offer only what you allow The [connect widget](https://selfstore.dev/docs/widgets) shows exactly the destinations you enable. A clinic app might allow an S3 bucket and WebDAV, and nothing else: ```html ``` Sharing is opt-in the strongest way there is: if you never mount the `` / `` widgets and never import their entry, that code is **absent from your bundle**, not merely hidden. A store that must never be shared simply never gains the ability. ## 5. The honest ceiling None of this changes the one limit no browser app escapes: your **origin**. Code running in your page - through XSS, a compromised dependency, a bad CDN - reads the decrypted data and the in-memory password, whatever the cache lock is set to. The hardening kit raises the floor a long way; it does not move that ceiling. Mitigate it the only way that works: - A **strict Content-Security-Policy**, no third-party scripts. - Subresource Integrity and, ideally, a reproducible build. - Ship only the entries you use (see step 4) to keep the attack surface small. The full analysis, including what each layer does and does not defeat, is the [security guide](https://selfstore.dev/docs/security) and the [threat model](https://github.com/selfstoredev/selfstore/blob/main/THREAT-MODEL.md). ## The whole thing, together ```ts import { selfstore, checkPasswordPolicy } from 'selfstore'; const policy = { minLength: 12, requireUppercase: true, requireDigit: true, requireSymbol: true }; const store = await selfstore('clinic-notes', { requireEncryption: true, // ciphertext-only leaves the device passwordPolicy: policy, // a strong password is enforced cacheLock: async ({ failed }) => promptForPassphrase({ failed }), // profile-copy-proof cache }); // Encryption + a policy-checked password, then a bucket the clinic controls. await store.protect(await promptForStrongPassword(policy)); await store.connectS3(clinicBucketConfig); ``` Map of this site for a model: https://selfstore.dev/llms.txt Every page in one file: https://selfstore.dev/llms-full.txt