<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akshay Sharma</title>
    <description>The latest articles on DEV Community by Akshay Sharma (@axaysharma).</description>
    <link>https://dev.to/axaysharma</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3877825%2F91698d38-c62d-4cdc-bda3-a9f446db6390.png</url>
      <title>DEV Community: Akshay Sharma</title>
      <link>https://dev.to/axaysharma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/axaysharma"/>
    <language>en</language>
    <item>
      <title>How I built a file-sharing tool where even I can't read your files (zero-knowledge architecture in Next.js)</title>
      <dc:creator>Akshay Sharma</dc:creator>
      <pubDate>Tue, 14 Apr 2026 05:53:34 +0000</pubDate>
      <link>https://dev.to/axaysharma/how-i-built-a-file-sharing-tool-where-even-i-cant-read-your-files-zero-knowledge-architecture-in-5h9a</link>
      <guid>https://dev.to/axaysharma/how-i-built-a-file-sharing-tool-where-even-i-cant-read-your-files-zero-knowledge-architecture-in-5h9a</guid>
      <description>&lt;p&gt;There's a moment most developers have experienced.&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;None of these give you control. The file outlives the intention.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;BurnShot&lt;/strong&gt; (&lt;a href="//burnshot.app"&gt;burnshot.app&lt;/a&gt;) - 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem with 'disappearing' file tools
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Neither is deletion. Both are theater.&lt;/p&gt;

&lt;p&gt;Real ephemeral sharing requires three things happening simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The file must be encrypted before it touches any server&lt;/li&gt;
&lt;li&gt;The server must never possess the decryption key&lt;/li&gt;
&lt;li&gt;Deletion must be synchronous — not a background job&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The encryption model: keys in the URL fragment
&lt;/h2&gt;

&lt;p&gt;When you upload a file on BurnShot, &lt;em&gt;AES-GCM&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

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

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

&lt;p&gt;This means if you subpoena my server, you get encrypted blobs. The keys don't exist there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auto-detonation: synchronous, not scheduled
&lt;/h2&gt;

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

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  OTP email verification without storing emails (you can share files without email verification also)
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;Neither system holds the complete puzzle. Thus, at any given point of time through strict separation of concerns, your data remains impenetrable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-download canvas rendering
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong in v1
&lt;/h2&gt;

&lt;p&gt;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.'&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
