secret.larger.io — 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.
- Zero-knowledge encryption — XChaCha20-Poly1305 (client) + AES-256-GCM (server)
- Text & files — Notes, documents, images, any file type. Up to 10 files per note, drag & drop.
- Burn after read — Destroyed after the first view
- Password protection — Optional, Argon2id key derivation
- Auto-expiry — 5 minutes to 30 days
- Read limits — Auto-delete after N reads
- Delete token — Manually delete a note at any time
- File previews — Images, PDF, video, audio rendered in-browser after decryption
- Chunked uploads — Stream large files in chunks with progress tracking (up to 500 MB)
- S3 storage — Optional S3-compatible backend (AWS, MinIO, R2) for large files
- QR codes — Share links easily on mobile
- i18n — 10 languages (en, fr, es, de, pt, it, ja, zh, ru, ko)
- Self-hostable — Single Docker container, customizable branding
git clone https://github.com/largerio/secret.git
cd secret
cp .env.example .env
# Generate a server encryption key (REQUIRED)
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Paste the output into .env as SERVER_ENCRYPTION_KEY
docker compose up -dOpen 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-jsimport { 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) │──blob──► │ 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 MBCompatible with AWS S3, MinIO, and Cloudflare R2.
docker compose pull # Pull new image
docker compose up -d # Restart
docker image prune -f # Clean upData 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 match MAX_CHUNKED_FILE_SIZE (or MAX_FILE_SIZE if chunked uploads are not used).
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 strictapps/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.