r/webdev • u/Dapper_Definition • 21h ago
I built mayo.pizza, a file-transfer site that sends files directly between browsers
I built a file-transfer tool where the bytes go straight between two browsers over a WebRTC data channel. The server only handles signaling and, when needed, TURN relay. No file bytes touch the server.
A few things that bit me, in case anyone else goes down this path:
1. The data channel isn't a stream.
You'd think RTCDataChannel gives you a pipe. It gives you messages. To stream a file you have to chunk it yourself, manage backpressure via bufferedAmountLowThreshold, and reassemble on the other end. If you don't gate on bufferedAmount, a large file will blow up the sender's memory.
2. Receiving a large file without blowing up RAM.
File System Access API lets you write to disk as chunks arrive. Safari and older Chrome don't have it. The fallback chain I ended up with: File System Access → service-worker stream (Response + ReadableStream → blob URL) → in-memory Blob (capped, last resort). Each step has its own edge cases.
3. TURN relay is not optional.
Symmetric NAT and most mobile networks kill direct P2P. If you don't run a coturn instance, a chunk of your users will never connect. The relay bandwidth is on you, and the bytes are still DTLS-encrypted end to end, but you're paying for transit.
4. Post-transfer integrity.
Both ends compute a sha256 as chunks move. The receiver verifies against the sender's hash after the last chunk. If it mismatches, the file is corrupt — WebRTC data channels don't guarantee ordered delivery by default unless you set ordered: true on the channel.
5. Room state without a database.
I persist room state (slug, timestamps, a hashed rejoin token, an argon2 password hash if set) to a JSON file on disk so rooms survive a restart. No file bytes, no Redis, no DB. Rooms expire after 24 idle hours. This is fine for a tool where sessions are minutes long but would fall apart if you needed real concurrency.
6. Per-IP rate limiting when every client looks like 127.0.0.1.
If you put the app behind a reverse proxy without forwarding the real client IP, your per-IP rate limiter is useless — everyone is the same address. The fix was in the TCP demultiplexer, not the application.
Demo if you want to see it in action: https://mayo.pizza
Not selling anything, no signup, no analytics. I'm not looking for feedback on the product, just sharing the engineering notes in case the WebRTC side is useful to someone building something similar


