DEV Community

Cover image for How I built a file-sharing tool where even I can't read your files (zero-knowledge architecture in Next.js)
Akshay Sharma
Akshay Sharma

Posted on

How I built a file-sharing tool where even I can't read your files (zero-knowledge architecture in Next.js)

There's a moment most developers have experienced.

You need to send a sensitive file - a private screenshot, a contract draft, a credential - and your options are: email it (it lives on both servers forever), WhatsApp it (metadata collected, file cached), or throw it on Google Drive (permanently indexed, permission revoke does nothing to cached versions).

None of these give you control. The file outlives the intention.

So I built BurnShot (burnshot.app) - a zero-knowledge, ephemeral sharing tool where files cryptographically erase themselves after a set view count or timer. Here's the architecture, the decisions, and the parts I got wrong.

The core problem with 'disappearing' file tools

Most tools that claim to offer disappearing or expiring files do one of two things: they either mark a file as 'deleted' in their database (but keep the bytes in storage), or they rely on the client to stop rendering the file without actually deleting it server-side.

Neither is deletion. Both are theater.

Real ephemeral sharing requires three things happening simultaneously:

  • The file must be encrypted before it touches any server
  • The server must never possess the decryption key
  • Deletion must be synchronous — not a background job

The encryption model: keys in the URL fragment

When you upload a file on BurnShot, AES-GCM encryption runs entirely in the browser using the Web Crypto API before the file is transmitted. The encrypted blob is what gets stored in Supabase Storage.

The decryption key is embedded in the shareable URL after the # (hash fragment). This is the critical part: URL fragments are never sent to the server in HTTP requests. They exist only in the browser. My server has never seen your key.

`// Simplified client-side encryption flow
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, fileBuffer
);
const exportedKey = await crypto.subtle.exportKey('raw', key);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(exportedKey)));

// Key goes into the URL fragment — never sent to server
const shareLink = https://burnshot.app/view/${fileId}#${keyBase64};`

This means if you subpoena my server, you get encrypted blobs. The keys don't exist there.

Auto-detonation: synchronous, not scheduled

Every file payload has a view_limit and a TTL stored in Postgres. Here's what happens on every access:
`-- Supabase RPC called on every view
UPDATE payloads
SET view_count = view_count + 1
WHERE id = $1
RETURNING view_count, view_limit, expires_at;

-- If threshold hit: immediate purge
SELECT storage.delete('payloads', file_ref);
DELETE FROM payloads WHERE id = $1;`

The deletion is atomic within the RPC. The file is gone before the HTTP response returns. There is no window where a race condition leaves the file accessible after its limit is hit — this was v1's critical bug, and I'll explain that shortly.

OTP email verification without storing emails (you can share files without email verification also)

If a sender wants to restrict access to a specific recipient, they add the recipient's email. Here's the privacy-preserving part: that email is SHA-256 hashed on the sender's device before it ever hits the API.

My database stores hash(email), never the plaintext. When the recipient enters their email to access the file, it gets hashed client-side and compared. I cannot tell you who the intended recipient was.

Ofcourse third party service for email - Resend has to know the plaintext email to deliver the message, and their logs will show the email body (the OTP). On Resend also I have set very short retention period which means this data gets deleted within few days.

But if a hacker breaches Supabase, they get encrypted blobs and hashed emails. They cannot read the files, and they don't know who the users are.
If a hacker breaches Resend, they get a list of email addresses and 6-digit numbers. But Resend has absolutely no access to the files, the decryption keys, or the database links.

Neither system holds the complete puzzle. Thus, at any given point of time through strict separation of concerns, your data remains impenetrable.

Anti-download canvas rendering

Files render inside a canvas element rather than native img or embed tags. This disables right-click save, browser download shortcuts, and most automated scrapers. It doesn't prevent screenshots — nothing on the web can. But it removes one-click exfiltration, which is the threat model for most use cases.

What I got wrong in v1

Version 1 ran file deletion in a background cron job every 5 minutes. The first comment on my Hacker News post was: 'Web based! receiver can take a screenshot very easily.'

They were right about the deletion window - there was a 5-minute gap after the final view where the file was still technically accessible. I rewrote the deletion logic to be synchronous within the RPC that same night. The screenshot comment was also valid — I added canvas rendering after that.

The second thing I got wrong: I launched with images only. Within weeks, the most requested feature was PDF support. Contracts, NDAs, payslips - the real sensitive sharing is documents, not photos. I added PDF support in v1.2 and it's now the majority of uploads.

The stack

  • Next.js 14 + TypeScript - App Router, server actions for API layer
  • Supabase — Postgres for metadata, Storage for encrypted blobs, Edge Functions for RPC
  • Vercel — Edge deployment, sub-100ms global response
  • Web Crypto API — all encryption/decryption in the browser

What's next

  • Watermarking: burning recipient identity into canvas renders for traceability
  • Audit trail for Enterprise without breaking zero-knowledge model
  • CLI tool for developers integrating ephemeral sharing into pipelines The tool is free at burnshot.app. Enterprise dedicated instances (isolated DB, custom domain) are available at $100/month for firms with compliance requirements.

Happy to go deep on any part of the architecture in the comments - the Web Crypto API has some gotchas around key serialization that took me a while to work through.

Top comments (1)

Collapse
 
shail_patel_be2c56251ebf4 profile image
Shail Patel

Really like how you broke down the client-side encryption flow. The fact that the server never sees the keys is what makes this actually trustworthy, not just ‘encrypted’ in marketing terms.”