Share passwords, notes, and files securely with end-to-end encryption. Your data is encrypted in the browser using XChaCha20-Poly1305 — the server never sees your content. Self-hosted with a single Docker container. No accounts, no tracking, no logs.
A modern, open-source alternative to PrivateBin, OneTimeSecret, and Yopass — built with Svelte 5, Hono, and TypeScript.
Live demo: secret.larger.io
messages/| Feature | Secret | PrivateBin | OneTimeSecret | Yopass |
|---|---|---|---|---|
| Zero-knowledge | Yes | Yes | No (server-side) | Yes |
| Client cipher | XChaCha20-Poly1305 | AES-256-GCM | — | OpenPGP |
| Server-side encryption | Yes (AES-256-GCM) | No | Yes | No |
| File attachments | Up to 10, 500 MB | Single, opt-in | No | Single, streaming |
| File previews | Image/PDF/AV | Image/PDF/media | No | No |
| Burn after read | Yes | Yes | Yes | Yes (toggleable) |
| Read limits (N reads) | Yes | No | No | No |
| Password protection | Yes (Argon2id) | Yes (PBKDF2) | Yes (passphrase) | Yes |
| Official SDK | Yes (JS/TS) | No | REST API only | CLI only |
| Stack | Svelte 5 + Hono | PHP | Ruby | Go + React |
| Deploy | Single Docker | PHP server | Ruby + Redis | Single Docker |
Comparison reflects publicly documented features at the time of writing. See each project's docs for the latest details.
git clone https://github.com/largerio/secret.git
cd secret
cp .env.example .env
# Generate a server encryption key (REQUIRED) — pick one
openssl rand -base64 32
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Paste the output into .env as SERVER_ENCRYPTION_KEY
docker compose up -d
Open http://localhost:3000. API documentation is available at /api/v1/docs (Scalar).
Use the JavaScript/TypeScript SDK to interact with any Secret instance programmatically:
npm install @secret/sdk-js
import { SecretClient } from "@secret/sdk-js";
const client = await SecretClient.create({
baseUrl: "https://secret.example.com",
apiKey: "your-api-key",
});
// Create a note
const { id, keyFragment } = await client.createNote({ text: "Hello, World!" });
const shareUrl = client.buildShareUrl(id, keyFragment);
// Read a note from a share URL
const parsed = SecretClient.parseShareUrl(shareUrl);
const { payload } = await client.readNote(parsed.id, parsed.keyFragment);
console.log(payload.text); // "Hello, World!"
Browser Server
┌──────────────────────┐ ┌──────────────────┐
│ 1. Generate key │ │ │
│ 2. Encrypt (XChaCha) │──ciphertext►│ 3. Encrypt (AES) │
│ │ │ 4. Store │
│ URL: /note/id#key │ │ │
│ └─ never sent │ │ Never sees key │
└──────────────────────┘ └──────────────────┘
The encryption key lives in the URL fragment (#key), which browsers never send to the server.
All settings via environment variables. See .env.example for the full list.
| Variable | Default | Description |
|---|---|---|
SERVER_ENCRYPTION_KEY |
— | Required. AES-256-GCM key (32 bytes, base64) |
APP_NAME |
Secret |
Application name |
APP_URL |
http://localhost:3000 |
Public URL |
APP_PRIMARY_COLOR |
#6366f1 |
Brand color |
MAX_FILE_SIZE |
10485760 |
Max file size in bytes (10 MB) |
MAX_FILES_PER_NOTE |
10 |
Max files per note |
MAX_EXPIRY |
604800 |
Max expiry in seconds (default: 7 days, max: 30 days) |
API_KEY |
— | API key for SDK clients (optional) |
API_KEY_1, API_KEY_2… |
— | Multiple API keys (optional) |
CHUNK_SIZE |
4194304 |
Chunk size for large uploads (4 MB) |
MAX_CHUNKED_FILE_SIZE |
524288000 |
Max chunked upload size (500 MB) |
PORT |
3000 |
Server port |
Warning: Never change
SERVER_ENCRYPTION_KEYafter deployment — all existing notes become unreadable.
Files are stored locally by default. For larger files, enable S3-compatible storage:
STORAGE_BACKEND=s3
S3_BUCKET=my-bucket
S3_REGION=us-east-1
S3_ENDPOINT=http://minio:9000 # MinIO / R2
S3_ACCESS_KEY_ID=your-key
S3_SECRET_ACCESS_KEY=your-secret
S3_FORCE_PATH_STYLE=true # Required for MinIO
MAX_FILE_SIZE=104857600 # 100 MB
Compatible with AWS S3, MinIO, and Cloudflare R2.
docker compose pull # Pull new image
docker compose up -d # Restart
docker image prune -f # Clean up
Data lives in a Docker volume — updates never delete your notes.
Caddy (automatic HTTPS):
secret.example.com {
reverse_proxy localhost:3000
}
Nginx:
server {
listen 443 ssl;
server_name secret.example.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 600M;
}
}
Set client_max_body_size to at least MAX_CHUNKED_FILE_SIZE (or MAX_FILE_SIZE if chunked uploads are not used). The example uses 600M as a safety margin above the 500 MB default.
pnpm install
pnpm dev # API + web dev servers
pnpm test # 422 tests, 100% backend coverage
pnpm lint # Biome lint + format
pnpm build # Production build
pnpm typecheck # TypeScript strict
apps/api/ Hono API (Node.js, SQLite, Drizzle ORM, OpenAPI)
apps/web/ SvelteKit frontend (Svelte 5, Tailwind CSS 4)
packages/sdk-js/ JS/TS SDK (SecretClient, encrypt/decrypt flows)
packages/crypto/ libsodium + AES-256-GCM encryption
packages/shared/ Zod schemas, types, constants, crypto test vectors
messages/ i18n (10 languages)
| Layer | Details |
|---|---|
| Client encryption | XChaCha20-Poly1305 (192-bit nonce, AEAD) |
| Server encryption | AES-256-GCM (defense-in-depth) |
| Password KDF | Argon2id (64 MiB, 3 iterations) |
| Write auth | PoW (Cap.js SHA-256) for browser, API keys for SDK |
| Token comparison | Timing-safe (crypto.timingSafeEqual) |
| Key hygiene | Zeroed after use (sodium.memzero) |
| Privacy | No IP logging, no cookies, no tracking |
| Database | SQLite secure_delete, WAL mode |
| Docker | Non-root, read-only filesystem, dropped capabilities |
| HTTP | Strict CSP, HSTS (preload), Permissions-Policy, per-IP rate limiting |
| Storage | Path traversal protection, S3 key validation |
| Validation | Zod schemas with max length constraints |
See SECURITY.md for the vulnerability disclosure policy.
See CONTRIBUTING.md.