# 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