What is this?

A service for sharing secrets once — passwords, API keys, private messages. Instead of sending secrets in plaintext via chat or email, Once generates a link that can be opened exactly once. After that, the secret is irrevocably deleted.

The key feature: end-to-end encryption ensures that the server never has access to the plaintext at any point. The encryption key is embedded in the URL fragment (#key) and is never sent to the server.

How It Works

  1. CLI encrypts the secret locally with a random AES-256-GCM key
  2. Server stores only the ciphertext with a public ID
  3. Link is generated: once.mathis-adler.dev/s/<id>#<key> — the key in the fragment never leaves the client
  4. Recipient opens the link, clicks “Reveal”, the browser fetches the ciphertext and decrypts it via WebCrypto
  5. Secret is consumed — atomically on first retrieval, then 410 Gone

Security Decisions

  • Zero-knowledge architecture — The server never sees the key or plaintext. Even in the event of a server compromise, the secrets remain protected.
  • POST instead of GET for consuming — Unfurl bots in Slack, Teams, or Discord only make GET requests. The reveal page (GET /s/:id) only shows the “Reveal” button without consuming the secret. Only the explicit POST /api/secrets/:id/consume returns the ciphertext.
  • Atomic consume — SQLite’s UPDATE ... RETURNING ensures that with concurrent requests, only the first one receives the ciphertext.
  • Content Security Policy — script-src 'self' prevents injected code from exfiltrating the key from the URL fragment.
  • 128-bit IDs — 22-character Base64url (~2^128 combinations) make brute-force attacks on IDs infeasible.
  • Two-tier rate limiting (Nginx + Express) — stricter for consume requests and invalid IDs.
  • Automatic cleanup — Expired secrets are deleted immediately; consumed ones after a one-hour grace period.

What I Learned

  • URL fragments (#...) are never sent to the server by the browser — making them ideal for client-side keys. This is not a hack but part of the HTTP specification (RFC 3986).
  • The WebCrypto API is powerful, but its ergonomics require care: key import, IV handling, and the correct separation of IV, ciphertext, and auth tag must be exact.
  • “Read once, then delete” sounds simple, but atomicity is critical. Without UPDATE ... RETURNING, there would be a race condition window where two requests could simultaneously receive the ciphertext.

Glossary

End-to-End Encryption
A communication principle where only the sender and recipient can read the message. The transport path — including the server — only sees encrypted data and has no access to the key.
AES-256-GCM
An authenticated encryption mode combining AES with a 256-bit key and Galois/Counter Mode. Provides both confidentiality and integrity protection — tampered ciphertexts are detected during decryption.
URL Fragment
The part of a URL after the # character. Per the HTTP specification, it is never sent to the server and is only processed client-side by the browser. This makes it suitable as a channel for data the server should not see.
Base64url
A URL-safe variant of Base64 encoding that replaces + with - and / with _. Can be used directly in URLs without URL encoding.
WebCrypto API
A browser API for cryptographic operations such as encryption, hashing, and key generation. Runs natively in the browser without external libraries and supports AES-GCM, RSA, ECDSA, and more.
Atomic Operation
An operation that either completes fully or not at all — with no intermediate states. In databases, atomicity prevents parallel requests from producing inconsistent results.
Content Security Policy
An HTTP header that tells the browser which resources a page is allowed to load. script-src 'self' allows only scripts from the same domain, blocking injected code (XSS).
Rate Limiting
A protection mechanism that limits the number of requests per time period. Prevents brute-force attacks and token enumeration through automated queries.