Self-hostable zero-knowledge pastebin. AES-256-GCM in the browser; the server never sees the key.
1.8K
Say it once. We'll forget.
A self-hostable, zero-knowledge pastebin for sharing secrets — text and files — that expire on their own. The server stores ciphertext and nothing else: the decryption key lives in the URL fragment and is never sent to it.
Try the demo · Documentation · Source and issues
The demo is a public instance with short retention and tight limits, reset without warning.
Most "send me the password" moments end in a chat message that stays there forever. shhh is for the other path: paste it, get a link, and have it disappear after a set number of reads or a deadline — without trusting the server with the contents.
#key=…),
which browsers never send to the server. Optional password protection derives a second key layer
with Argon2id, combined with the fragment key.Also in the box: two-factor authentication (TOTP) with backup codes, invitations, an IP allowlist and blocklist, email sharing at creation time, Cloudflare Turnstile, per-IP and per-account rate limiting, account deletion with full cascade, English and French.
| Tag | Moves |
|---|---|
latest | newest release, never a pre-release |
1 | newest 1.x — fixes and features, never a breaking 2.0 |
1.0 | newest 1.0.x — fixes only |
1.0.0 | exactly that release, immutable |
Built for linux/amd64 and linux/arm64.
The documentation site ships as a separate image, thodadev/shhh-docs,
tagged with the same version so the two always match. It needs no database and no configuration —
run it if you want the docs on your own network, otherwise read them at the link above.
# docker-compose.yml
name: shhh
services:
app:
image: thodadev/shhh:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://shhh:CHANGE_ME@db:5432/shhh
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?generate with openssl rand -base64 32}
BETTER_AUTH_URL: https://shhh.example.com
# Proxies you control in front of the app. 0 ignores X-Forwarded-For and uses the connection
# address; set it to 1 behind a single reverse proxy, 2 behind Cloudflare plus your own.
TRUSTED_PROXY_DEPTH: 1
NUXT_PUBLIC_TURNSTILE_SITE_KEY: ${NUXT_PUBLIC_TURNSTILE_SITE_KEY:-}
NUXT_TURNSTILE_SECRET_KEY: ${NUXT_TURNSTILE_SECRET_KEY:-}
MAIL_PROVIDER: none
depends_on:
db:
condition: service_healthy
db:
image: postgres:18-alpine
restart: unless-stopped
environment:
POSTGRES_USER: shhh
POSTGRES_PASSWORD: CHANGE_ME
POSTGRES_DB: shhh
volumes:
- db-data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shhh -d shhh"]
interval: 5s
timeout: 5s
retries: 10
volumes:
db-data:
docker compose up -d
Open your instance and the setup wizard creates the super admin account and the initial limits. Migrations are applied automatically on every boot.
An installer that generates the secrets and writes the .env for you lives in the repository:
curl -fsSLO https://raw.githubusercontent.com/thoda-dev/shhh/master/install.sh
less install.sh && sh install.sh
Secrets and infrastructure live in the environment; everything else lives in the database and is
edited from /admin/settings — retention, max reads, sizes, quotas, rate limits, public
registration, anonymous pastes, enforced 2FA, invitation expiry.
| Variable | |
|---|---|
DATABASE_URL | Required. PostgreSQL connection string. |
BETTER_AUTH_SECRET | Required. Signs session cookies — openssl rand -base64 32. Changing it logs everybody out. |
BETTER_AUTH_URL | Required. Public URL of the instance, no trailing slash. Also builds the links sent by email. |
NUXT_PUBLIC_TURNSTILE_SITE_KEY NUXT_TURNSTILE_SECRET_KEY | Cloudflare Turnstile. Paste creation is rejected without a valid token, signed-in users included, so an instance without these can be read but not written to. |
MAIL_PROVIDER | none, resend or smtp. none is supported: the instance works, but email verification, password resets, invitations and email sharing are unavailable. |
MAIL_FROM, RESEND_API_KEY, MAIL_SMTP_* | Depending on the provider. |
PORT | Host port. The container always listens on 3000. |
Terminate TLS at your proxy and set BETTER_AUTH_URL to the public HTTPS address. The proxy must
overwrite X-Forwarded-For rather than append to a client-supplied value — rate limiting and IP
banning both trust that header.
GET /api/health returns database status, mail provider and storage usage. Point Uptime Kuma at it.
The image also declares a HEALTHCHECK against it, so docker ps reports an instance that has lost
its database as unhealthy rather than merely running.
The threat model, its boundaries, and the one place the decryption key deliberately reaches the server (email sharing at creation time) are documented in SECURITY.md.
MIT — © 2026 Thoda
Content type
Image
Digest
sha256:8ddc7a662…
Size
62.5 MB
Last updated
6 days ago
docker pull thodadev/shhh