Shh
Self-destructing secret sharing, hosted on your own hardware
Shh is one-time secret sharing you run yourself. Paste a password, pick an expiry, and you get back a private URL. The recipient opens it, clicks to reveal, and — if you chose first_view — the secret is deleted in the same transaction that decrypted it.
The server never holds the key
On create, the server generates a random AES-256-GCM key, encrypts the plaintext, and stores only the ciphertext in SQLite. The key is never persisted. It goes into the URL fragment — the part after the # — which browsers do not transmit, so it never lands in a request log, a proxy log, or an access record. On reveal, the client sends the key back in the request body to decrypt.
That single design decision is what makes the threat model tractable, so the README states it plainly rather than gesturing at “military-grade encryption”:
- Database-only compromise — nothing is recoverable. The keys are not in the database.
- Database plus the URL — the secret is recoverable. If a password was set, that must also be cracked (bcrypt, 12 rounds).
- Server compromise during a reveal — an attacker controlling the server can intercept the plaintext. The app cannot defend against that, and says so.
- Link-preview burn — revealing requires a click, so Slack and iMessage prefetching a URL will not burn a first-view secret.
- On-device cache — the service worker's cache is opt-in. Only immutable build output and icons are ever written;
/s/*,/created/*and/api/*are hard-denied before any caching strategy runs, so a revealed secret cannot be replayed from disk after it self-destructs.
A versioned API, because clients exist
There is a versioned HTTP API at /api/v1 for programmatic clients — a CLI, a script, or the Omarchy bar widget built against it. Unlike the browser route, create returns a complete shareable URL: a desktop client has no window.location, and an instance behind a proxy cannot infer its own public origin.
Because anyone can self-host it, a client is required to treat the server address as user configuration and validate it against /api/v1/info rather than hardcoding one. Auth is optional and off by default; setting SHH_API_TOKENS requires a bearer token on /api/v1 while leaving the browser routes open, since the web UI has nowhere to hide a token.
Stack
| Framework | Next.js 16 with TypeScript, installable as a PWA |
|---|---|
| Crypto | AES-256-GCM per secret; bcrypt (12 rounds) for optional passwords |
| Storage | SQLite — ciphertext only, with a sweeper that runs every minute |
| Limits | 100 KB plaintext; 10 reveal attempts per IP+id per 5 min; 60 creates per IP per hour |
| Proxy awareness | SHH_TRUSTED_PROXY_HOPS, so rate limits key to the real client rather than a forgeable header |
| Deploy | Multi-arch GHCR images (amd64, arm64), or a systemd + nginx deploy to a Pi |