<?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: iTroy0</title>
    <description>The latest articles on DEV Community by iTroy0 (@itroy0).</description>
    <link>https://dev.to/itroy0</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%2F3866314%2Faf023998-ef7e-4df3-bc3e-4d84ebcb8ef9.jpeg</url>
      <title>DEV Community: iTroy0</title>
      <link>https://dev.to/itroy0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itroy0"/>
    <language>en</language>
    <item>
      <title>The Manifest - Zero Server P2P file sharing with no traces</title>
      <dc:creator>iTroy0</dc:creator>
      <pubDate>Tue, 07 Apr 2026 17:37:06 +0000</pubDate>
      <link>https://dev.to/itroy0/the-manifest-zero-server-p2p-file-sharing-with-no-traces-3j51</link>
      <guid>https://dev.to/itroy0/the-manifest-zero-server-p2p-file-sharing-with-no-traces-3j51</guid>
      <description>&lt;p&gt;So I got tired of file sharing apps.&lt;/p&gt;

&lt;p&gt;Every time I want to send someone a file, I have to upload it to some server, wait, grab a link, and hope the service doesn't&lt;br&gt;
  compress my stuff, slap a size limit on it, or require the other person to make an account. Oh, and my file now lives on someone&lt;br&gt;
  else's computer forever. Cool.&lt;/p&gt;

&lt;p&gt;I wanted something stupid simple: drop a file, get a link, send it. The file goes directly from my browser to theirs. When I&lt;br&gt;
  close the tab, it's gone. No server ever touches it.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://the-manifest-portal.vercel.app/" rel="noopener noreferrer"&gt;https://the-manifest-portal.vercel.app/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What it actually does&lt;/p&gt;

&lt;p&gt;You open the site, drop your files, and get a portal link + QR code. Send that to whoever. They open it, see the file list, and&lt;br&gt;
  pick what they want to download. Files stream directly from your browser to theirs over WebRTC.&lt;/p&gt;

&lt;p&gt;That's it. No upload. No waiting. No account. Close the tab and the portal is gone like it never existed.&lt;/p&gt;

&lt;p&gt;The interesting parts (technically)&lt;/p&gt;

&lt;p&gt;There's no backend. The whole thing is a static React app on Vercel. PeerJS handles WebRTC signaling, and after that it's a&lt;br&gt;
  direct browser-to-browser connection. The "server" is literally just a bundle of HTML/JS/CSS.&lt;/p&gt;

&lt;p&gt;Double encryption. WebRTC already encrypts with DTLS, but I added another layer on top — ECDH key exchange + AES-256-GCM on every&lt;br&gt;
   chunk. Why? Because if someone's behind a strict NAT and has to use a TURN relay, I don't want even the relay to see the data.&lt;br&gt;
  Both sides can verify a key fingerprint to make sure nobody's sitting in the middle.&lt;/p&gt;

&lt;p&gt;No file size limit. This was a fun problem. You can't just shove a 2GB file into memory in the browser. So files get chunked into&lt;br&gt;
   256KB pieces, streamed through the WebRTC data channel with backpressure control, and on the receiving end, StreamSaver.js pipes&lt;br&gt;
   them directly to disk. The browser never holds the full file in RAM.&lt;/p&gt;

&lt;p&gt;Streaming zip. If someone clicks "Download All as Zip", it doesn't build the zip in memory either. It uses fflate's streaming Zip&lt;br&gt;
   class piped through StreamSaver — chunks flow through and get written to disk as they arrive. Zero accumulation.&lt;/p&gt;

&lt;p&gt;Resume on disconnect. The receiver tracks the last chunk it got. If the connection drops, it reconnects and sends a resume&lt;br&gt;
  message with the chunk index. The sender picks up where it left off.&lt;/p&gt;

&lt;p&gt;Things I added because people asked&lt;/p&gt;

&lt;p&gt;I posted this on Reddit and got some good feedback that turned into actual features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple recipients — Originally it was one person at a time. Now unlimited people can connect simultaneously, each with their
own encrypted channel and independent progress tracking. That was a fun refactor.&lt;/li&gt;
&lt;li&gt;Password protection — Optional. Set a password on your portal and the receiver has to enter it before they see anything.&lt;/li&gt;
&lt;li&gt;Live chat — Since the data channel is already open and encrypted, I added bidirectional chat. Each receiver gets an
auto-generated nickname (SwiftFox42, BoldOwl17, etc.) so you can tell who's who. Messages relay between all connected receivers
too, so it's basically a group chat.&lt;/li&gt;
&lt;li&gt;Connection quality — Polls RTCPeerConnection stats every 3 seconds and shows RTT latency with a color-coded badge. Green is
good, yellow is meh, red means you're going to have a bad time.&lt;/li&gt;
&lt;li&gt;Per-file downloads — The receiver sees the full file list and picks what they want. No forced bulk downloads. Each file streams
independently.&lt;/li&gt;
&lt;li&gt;TURN relay opt-in — If direct P2P fails (happens a lot with certain ISPs), the user gets an explanation of what a relay is and
can opt in. Still encrypted. I self-host coturn on a cheap VPS for this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stack&lt;/p&gt;

&lt;p&gt;React 19, Vite, PeerJS, Web Crypto API, StreamSaver.js, fflate, dnd-kit, Tailwind v4. No backend, no database. The whole thing&lt;br&gt;
  deploys as a static site.&lt;/p&gt;

&lt;p&gt;What I learned&lt;/p&gt;

&lt;p&gt;WebRTC is simultaneously amazing and painful. The fact that you can open a direct encrypted connection between two random&lt;br&gt;
  browsers on the internet is incredible. The fact that it fails on half the networks in Pakistan because of symmetric NATs is less&lt;br&gt;
   incredible. Hence the TURN relay.&lt;/p&gt;

&lt;p&gt;React StrictMode and WebRTC don't play nice. Double-mounting effects means double peer creation, which means the second one&lt;br&gt;
  stomps the first one's connection. Spent way too long debugging "it says connected but the other side says portal closed" before&lt;br&gt;
  adding destroyed flags everywhere.&lt;/p&gt;

&lt;p&gt;Browser differences are real. StreamSaver works great on Chrome/Edge but falls back to in-memory blobs on Safari/Firefox. You&lt;br&gt;
  have to handle both paths gracefully.&lt;/p&gt;

&lt;p&gt;Try it / contribute&lt;/p&gt;

&lt;p&gt;Live: &lt;a href="https://the-manifest-portal.vercel.app/" rel="noopener noreferrer"&gt;https://the-manifest-portal.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/iTroy0/TheManifest" rel="noopener noreferrer"&gt;https://github.com/iTroy0/TheManifest&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's AGPL-3.0 — open source, free forever. If you find bugs or have ideas, open an issue. If you find it useful,&lt;br&gt;
  &lt;a href="https://buymeacoffee.com/itroy0" rel="noopener noreferrer"&gt;https://buymeacoffee.com/itroy0&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
