What is a one-time secret link?
It is a URL that reveals a secret to the first person who opens it and then destroys the stored copy. It suits credentials that must reach one person and should not sit in a chat history or inbox afterwards.
DEVOPS UTILITIES / ALL TOOLS
Send a password, API token, or private note as a self-destructing link. The secret is encrypted in your browser, only the ciphertext is uploaded, and the link works exactly once before the stored copy is deleted.
ZERO-KNOWLEDGE DESIGN
When you create a link, this page generates a fresh 256-bit AES-GCM key and a random 12-byte initialization vector, encrypts your text with the Web Crypto API, and joins the initialization vector and ciphertext into one base64url string. That string is the only thing uploaded. The key is exported separately and appended to the link after a #, so the finished URL carries the key while the request that created it did not.
Nothing is reused between secrets. Every link has its own key and its own initialization vector, so recovering one secret tells an attacker nothing about any other.
DATA BOUNDARY
The distinction that matters is not a promise about behaviour, it is what the service is structurally capable of holding. Without the key, the stored bytes are not decryptable by the operator, by anyone who compromises the store, or by anyone served a subpoena for it.
| Value | Sent to the service | Reason |
|---|---|---|
| The plaintext secret | Never | It is encrypted in this browser before any request is made. |
| The encryption key | Never | It is placed after the # in the link, and browsers do not send fragments to servers. |
| The ciphertext | Yes | Stored as opaque bytes the service has no way to read. |
| The chosen lifetime | Yes | Needed so the record can expire on its own. |
| The secret identifier | Yes | A random handle used to look the record up once. |
WHY THE HASH MATTERS
Everything after the # in a URL is the fragment. It is a client-side addressing feature, and browsers strip it before building the HTTP request, so it never appears in a request line, a proxy log, an access log, or a referrer header. Putting the key there is what makes the key travel with the link while staying invisible to the server.
It is also the reason a truncated link is fatal. If a chat client, mail scanner, or copy-and-paste drops the part after the #, the ciphertext still exists but no key does, and no one can recover it.
SELF-HOSTED BACKEND
The storage service runs on Contabo K3s rather than a managed provider. DNS for api.gajan.dev is proxied through Cloudflare, TLS terminates at Traefik with cert-manager, and records live in memory-only Valkey. Ciphertext is never written to disk, so there is no file, snapshot, or backup to recover it from later.
That is a deliberate trade. It means an unread secret does not survive a restart of the store, and it means occasional downtime on self-hosted hardware is normal. When the service is unreachable, nothing is lost that was ever readable.
EXACTLY ONCE
Retrieval is a single atomic operation: the record is fetched and removed together, not read first and deleted afterwards. If two requests arrive at the same instant, one of them returns the ciphertext and the other returns the same not-found response as a link that never existed.
Not-found is deliberately indistinguishable across three cases: the secret never existed, it was already opened, or it expired. Distinguishing them would let someone probe which identifiers were real.
This page also never fetches a secret on load. The reveal page requires an explicit click, so a link preview, a security scanner, or a browser prefetching the URL cannot consume the secret before the recipient reads it.
LIFETIME
Every secret carries a lifetime chosen at creation: 1 hour, 24 hours, or 7 days. The record is removed when that lifetime elapses even if nobody opened it, so an abandoned link becomes a dead link rather than a credential sitting in a queue indefinitely.
Choose the shortest window the recipient can realistically work with. A one-hour link that is resent is safer than a seven-day link that sits unopened in an inbox over a weekend.
HONEST LIMITS
Encryption in the browser removes one specific risk, which is the storage service reading your secret. It does not remove the rest:
Used well, this is a way to hand one credential to one person and have the copy disappear afterwards. Rotate anything you share this way once it has served its purpose.
FREQUENTLY ASKED QUESTIONS
It is a URL that reveals a secret to the first person who opens it and then destroys the stored copy. It suits credentials that must reach one person and should not sit in a chat history or inbox afterwards.
No. The secret is encrypted in your browser with AES-GCM before the request is sent, and the key is never transmitted. The service holds ciphertext it has no key for.
Everything after the # is the URL fragment. Browsers keep fragments local and never place them in the HTTP request, so the key can travel inside the link without ever reaching the service that stores the ciphertext.
Only the first reader gets the secret. Retrieval and deletion happen as one atomic operation, so exactly one request can win; everyone after that sees the same not-found response as an expired or never-existing link.
You choose 1 hour, 24 hours, or 7 days. When that time passes the record is removed whether or not it was read, so an unopened link simply stops working.
Something opened it first. Link previews, mail security scanners, and archiving tools all follow URLs. The reveal page never retrieves a secret automatically, so merely loading the link does not consume it, but any tool that goes on to perform the reveal request would.
The fragment is the key, so a link cut short after the # is unrecoverable. There is no copy of the key anywhere else. Create a new secret and send the complete link.
On a Contabo VPS running a self-hosted K3s cluster at api.gajan.dev, with Cloudflare DNS and proxy in front and TLS terminated at Traefik via cert-manager. Ciphertext sits in a memory-only Valkey store, is never written to disk, and disappears on read, on expiry, or if the store restarts.
No. It is a transport for handing a credential to one person once. Long-lived credentials belong in a password manager or a secrets manager, and anything shared this way should still be rotated afterwards.