So I got tired of file sharing apps.
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
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
else's computer forever. Cool.
I wanted something stupid simple: drop a file, get a link, send it. The file goes directly from my browser to theirs. When I
close the tab, it's gone. No server ever touches it.
So I built https://the-manifest-portal.vercel.app/.
What it actually does
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
pick what they want to download. Files stream directly from your browser to theirs over WebRTC.
That's it. No upload. No waiting. No account. Close the tab and the portal is gone like it never existed.
The interesting parts (technically)
There's no backend. The whole thing is a static React app on Vercel. PeerJS handles WebRTC signaling, and after that it's a
direct browser-to-browser connection. The "server" is literally just a bundle of HTML/JS/CSS.
Double encryption. WebRTC already encrypts with DTLS, but I added another layer on top — ECDH key exchange + AES-256-GCM on every
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.
Both sides can verify a key fingerprint to make sure nobody's sitting in the middle.
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
256KB pieces, streamed through the WebRTC data channel with backpressure control, and on the receiving end, StreamSaver.js pipes
them directly to disk. The browser never holds the full file in RAM.
Streaming zip. If someone clicks "Download All as Zip", it doesn't build the zip in memory either. It uses fflate's streaming Zip
class piped through StreamSaver — chunks flow through and get written to disk as they arrive. Zero accumulation.
Resume on disconnect. The receiver tracks the last chunk it got. If the connection drops, it reconnects and sends a resume
message with the chunk index. The sender picks up where it left off.
Things I added because people asked
I posted this on Reddit and got some good feedback that turned into actual features:
- 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.
- Password protection — Optional. Set a password on your portal and the receiver has to enter it before they see anything.
- 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.
- 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.
- Per-file downloads — The receiver sees the full file list and picks what they want. No forced bulk downloads. Each file streams independently.
- 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.
The stack
React 19, Vite, PeerJS, Web Crypto API, StreamSaver.js, fflate, dnd-kit, Tailwind v4. No backend, no database. The whole thing
deploys as a static site.
What I learned
WebRTC is simultaneously amazing and painful. The fact that you can open a direct encrypted connection between two random
browsers on the internet is incredible. The fact that it fails on half the networks in Pakistan because of symmetric NATs is less
incredible. Hence the TURN relay.
React StrictMode and WebRTC don't play nice. Double-mounting effects means double peer creation, which means the second one
stomps the first one's connection. Spent way too long debugging "it says connected but the other side says portal closed" before
adding destroyed flags everywhere.
Browser differences are real. StreamSaver works great on Chrome/Edge but falls back to in-memory blobs on Safari/Firefox. You
have to handle both paths gracefully.
Try it / contribute
Live: https://the-manifest-portal.vercel.app/
Source: https://github.com/iTroy0/TheManifest
It's AGPL-3.0 — open source, free forever. If you find bugs or have ideas, open an issue. If you find it useful,
https://buymeacoffee.com/itroy0.
Top comments (0)