You need to send someone an .env file.
Email may keep it long after you forget sending it. A chat attachment may get copied into backups, previews, and retention policies you have never read. A public paste is obviously a terrible idea. So you zip it, add a password, send the password in another message, and pretend this is a normal way to work in 2026.
I wanted a smaller answer: a link that expires, a server that never receives the decryption key, and something I can deploy to my own Cloudflare account.
So I built Ephemeral Sharing.
The server only gets ciphertext
Each chunk is encrypted in the browser before it leaves for Cloudflare. The browser creates a random key, encrypts the content with AES-256-GCM, and uploads only encrypted bytes to R2.
The key goes in the URL fragment:
https://files.example.com/f/abc#key
Browsers do not send the fragment after # in an HTTP request. The Worker receives /f/abc, but it does not receive key. The recipient's browser reads the fragment and decrypts the file locally.
The split looks like this:
plaintext
-> browser encryption
-> ciphertext in R2
expiry + download count
-> KV
decryption key
-> URL fragment
-> recipient browser
That boundary is the point of the project. R2 stores bytes it cannot turn back into the original file without the link.
What Cloudflare can still see
Client-side encryption does not make the server blind to everything.
| Data | Visible to the server |
|---|---|
| File contents | No, R2 stores ciphertext |
| Filename and media type | No, they are in the encrypted header |
| File size | Yes |
| Expiry | Yes |
| Download count | Yes |
| Password protection enabled | Yes |
I would rather name that metadata than hide it behind a vague claim about zero knowledge. The file contents stay encrypted. The service still needs enough information to enforce limits and clean up storage.
The Worker also stores whether password protection is enabled so the recipient page knows to ask for one. It does not receive the password.
When a link expires or reaches its download limit, the Worker removes access first. An hourly scheduled sweep deletes the ciphertext from R2 afterward, so link expiry and physical deletion are not the same instant.
The link is a secret
Anyone with the full link can decrypt the share while it remains active. Expiry helps. A download limit helps. Neither makes a leaked link harmless.
There is also an optional password. When you add one, the browser derives the encryption key from both the link key and the password. The password never goes to the server, and the link is not enough on its own.
Send the link and password through separate channels if the file warrants it. For a low-risk note, the link by itself may be enough. I am not trying to turn every screenshot into a spy movie.
Text shares do not get burned by previews
The Text tab is for API keys, connection strings, .env snippets, and short notes. Text shares default to one read.
One annoying edge case is link previews. Chat applications often open links to generate a title and image. If fetching the page consumed the only read, the recipient would get an expired link before clicking it.
Ephemeral Sharing waits for the recipient to press Reveal text before fetching the ciphertext. A preview can open the page without spending the read.
After decryption, the recipient can copy the text or save it as a file. That last part matters. The service can limit access, but it cannot make someone forget data they already revealed.
Files, bundles, and the command line
The web interface accepts one file, several files, or plain text. A bundle stores each file as a separate encrypted object plus an encrypted index. Recipients can download one item or the full set, and revoking the bundle removes the index and its member objects.
The site is also installable as a PWA. Installed mode can remember the upload token on that device. A normal browser tab keeps the token for the session unless you ask it to remember.
For shell workflows, the repository includes tools/ef.mjs:
export EF_HOST=https://files.example.com
export EF_TOKEN=your-upload-token
node tools/ef.mjs put ./backup.tar.gz --ttl 3600 --limit 1 --password hunter2
node tools/ef.mjs get 'https://files.example.com/f/abc#key' -o ./backup.tar.gz -p hunter2
node tools/ef.mjs put - --name .env --limit 1 < .env
node tools/ef.mjs get 'https://files.example.com/f/abc#key' > .env
The CLI does the encryption locally too. A plain curl request to the download API only gets ciphertext because the key is still in the fragment.
Large files get split into chunks
The browser splits plaintext into 8 MiB chunks and encrypts each chunk separately. R2 multipart upload receives one encrypted chunk per part.
The chunk index and final-chunk flag are authenticated data. Reordering chunks, dropping one, duplicating one, or moving a chunk between files causes decryption to fail. The encrypted header also records the expected chunk count, which catches truncation.
This is the part that took the project beyond "put a Blob in R2 and add a timer." File encryption is straightforward until files stop fitting comfortably in memory.
Deploy it to your own account
The repository has a Deploy to Cloudflare button. It creates a copy of the repository and provisions the R2 bucket and KV namespace declared in wrangler.toml.
Cloudflare will ask for UPLOAD_TOKEN. Generate a long random value locally:
npm run token
Copy the generated value. Cloudflare's deploy flow will prompt for it.
If you prefer the manual route, clone the repository and create the storage resources:
git clone https://github.com/bnap00/cf-ephemeral-share.git
cd cf-ephemeral-share
npm install
npx wrangler login
npx wrangler r2 bucket create ephemeral-files
npx wrangler kv namespace create META
Paste the KV namespace ID into wrangler.toml. Run the token command, copy its output, and paste that value when Wrangler prompts for the secret:
npm run token
npx wrangler secret put UPLOAD_TOKEN
npm run deploy
Uploads fail closed when UPLOAD_TOKEN is missing. Existing links can still be served, but nobody can add new content.
One read is a guardrail, not a lock
Download counting uses a read-modify-write operation in KV, which is eventually consistent. A location can read stale metadata for 60 seconds or more, and KV allows one write to the same key per second.
Requests do not need to arrive at the exact same instant to exceed the limit. Requests from different locations can see the same count, and a burst can outrun or fail a count update.
That means the limit is useful for normal sharing, but it is not strict mutual exclusion. If your threat model depends on mathematically guaranteeing that only one request ever receives bytes, this project does not provide that guarantee.
I could hide that caveat and put a large "BURN AFTER READING" label on the button. The label would look cool. The system would still have the same race.
What this does not protect
Browser encryption has a hard limit: the deployment serves the JavaScript that performs the encryption. If someone compromises your Worker and changes that JavaScript, they can steal plaintext or keys before encryption.
Self-hosting reduces how many parties you trust. It does not remove trust from the code you serve. Review changes before deploying them. The repository also includes the CLI for cases where you do not want to use JavaScript delivered by the site.
Ephemeral Sharing also cannot stop a recipient from copying a revealed secret, taking a screenshot, or forwarding the link. It controls access to the service. It does not control the recipient's machine.
Small enough to inspect
There is no application build step and no runtime package dependency. The deployed application is a Worker plus static files in public/. Wrangler and Playwright are development dependencies.
I wanted this to remain small enough that I can inspect the trust boundary without excavating a framework stack first. Security code is not automatically safe because it is short, but shorter code gives me fewer places to hide a bad assumption.
The useful question for a privacy tool is not whether it has encryption somewhere. It is which component sees plaintext, where the keys travel, and what metadata remains after the file expires.
Ephemeral Sharing gives me a concrete answer to those questions, running inside an account I control.
Feel free to connect or reach out if you have questions, threat-model holes, or a weird file-sharing workflow I should test.