Private, server-free P2P file transfer in the browser.
No uploads. No cloud storage. No file size limits. Files stream directly from sender to receiver over an encrypted WebRTC channel — the server only exchanges a small handshake so the two browsers can find each other, then steps aside.
Live: filecourier.netlify.app
FileCourier lets two people transfer a file of any size directly between their browsers without uploading it anywhere. You select the file, share a link, your friend opens it, and the bytes flow straight from your device to theirs. No account, no cloud, no size cap.
- Zero server storage — file data never leaves the two browsers
- No size limit — Chrome/Edge stream directly to disk; other browsers support up to 512 MB
- End-to-end encrypted — WebRTC enforces DTLS-SRTP on every data channel
- No account needed — share one link, start transferring
- Transfer controls — both sender and receiver can cancel at any time
- Cross-network — works through NAT, hotspots, and most firewalls via automatic TURN relay fallback
- Mobile-friendly — responsive layout, works on phones and tablets
- Sender opens the app — a unique share link is generated instantly
- Sender copies the link and sends it to the receiver (WhatsApp, SMS, email — anything)
- Receiver opens the link in their browser
- A direct WebRTC DataChannel is established between the two browsers
- The file streams in 64 KB chunks from sender to receiver
- Receiver's browser saves chunks directly to disk as they arrive — no waiting for the full file
Sender browser Receiver browser
│ │
│ ① Register peer ID │
├──────────────────────────────────────►│ 0.peerjs.com
│ PeerJS signaling server │ (PeerJS public cloud)
│ │
│ ② ICE candidate exchange │
│◄─────────────────────────────────────►│ (metadata only, ~200 bytes total)
│ │
│ ③ WebRTC DataChannel (P2P or relay) │
│◄═════════════════════════════════════►│
│ │
│ ④ File chunks (64 KB × N) │
├──────────────────────────────────────►│
The signaling server is only used to exchange peer IDs and ICE candidates (step ② above). After the WebRTC channel opens, the signaling server is no longer involved.
WebRTC uses ICE (Interactive Connectivity Establishment) to find the best path between two peers behind NAT routers:
| Phase | Method | Description |
|---|---|---|
| 1 | STUN | Each browser contacts a STUN server to discover its public IP. Both sides exchange these addresses and attempt a direct hole-punch. Succeeds for ~85–90% of home broadband connections. |
| 2 | TURN relay | If direct fails (symmetric NAT, CGNAT, strict firewall), both browsers connect to a TURN relay server. All data flows through the relay, encrypted. Slower than direct, but works through almost any network restriction. |
FileCourier tries each tier in order, falling through to the next if unsupported:
| Tier | API | Browsers | Size limit |
|---|---|---|---|
| 1 | StreamSaver.js | Chrome, Edge | Unlimited — streams to disk |
| 2 | File System Access API | Chrome, Edge | Unlimited — streams to disk |
| 3 | In-memory Blob | All browsers | 512 MB — held in RAM then saved |
| Connection type | Typical success | How |
|---|---|---|
| Home WiFi ↔ Home WiFi (different ISPs, anywhere in the world) | ~90% | Direct P2P via STUN — no relay needed |
| Broadband ↔ Broadband (India, US, EU, UK, Australia, etc.) | ~90% | Direct P2P via STUN — no relay needed |
| Mobile data (major global carriers) ↔ WiFi | ~70–80% | TURN relay handles CGNAT automatically |
| Same-network transfers (same WiFi or LAN) | ~99% | Direct P2P, shortest possible path |
| Scenario | Reason | What happens |
|---|---|---|
| Symmetric NAT (some ISPs/routers) | Direct hole-punch impossible | TURN relay takes over — transfer still works |
| Corporate / university network | Strict outbound firewall | TURN relay usually gets through on port 443 |
| Mobile data behind CGNAT | Can't accept inbound connections | TURN relay handles it; see below for exceptions |
A small number of mobile carriers apply IP-level filtering that blocks the free TURN relay servers used by FileCourier. When this happens:
- Direct P2P fails (CGNAT blocks it)
- Relay also fails (carrier blocks the relay server IP ranges)
Workaround: Connect the mobile device to WiFi instead of mobile data. The connection will establish immediately.
This behaviour has been observed on certain South/South-East Asian carriers. It is not a fundamental limitation of the technology — it is carrier-specific. Major global carriers (Jio, Airtel, Vodafone, T-Mobile, AT&T, Comcast, BT, Telstra, etc.) do not exhibit this.
If a direct connection does not open within 8 seconds:
- A yellow "Connect via Relay" button appears on the receiver's screen
- Receiver clicks it — the app reconnects through TURN relay servers
- The sender's link stays valid — no need to refresh or generate a new link
- The sender's screen shows an advisory to wait; no action needed from sender
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| P2P transfer | ✅ | ✅ | ✅ | ✅ |
| StreamSaver (unlimited size, stream to disk) | ✅ | ✅ | ❌ | ❌ |
| File System Access API | ✅ | ✅ | ❌ | ❌ |
| In-memory fallback (≤ 512 MB) | ✅ | ✅ | ✅ | ✅ |
Recommendation: Use Chrome or Edge when sending or receiving files larger than 512 MB.
FileCourier/
├── index.html # Landing page — features, FAQ, how-it-works
├── app.html # Transfer app shell — loads all scripts
├── netlify.toml # Security headers + /app → /app.html rewrite
├── assets/
│ ├── css/
│ │ └── style.css # Full design system (CSS custom properties, all components)
│ └── js/
│ ├── config.js # All tunable constants: TURN servers, timeouts, chunk sizes
│ ├── i18n.js # UI strings — FC.t() lookup used by app.js
│ └── app.js # P2P transfer controller (~960 lines)
└── peerserver/
├── index.js # PeerJS signaling server (deploy to Render.com)
└── package.json
window.FC namespace — all three JS files extend window.FC = window.FC || {}. No bundler, no build step — pure browser JavaScript.
Deferred load order — app.html loads scripts with defer in this order: PeerJS CDN → StreamSaver CDN → config.js → i18n.js → app.js. DOMContentLoaded fires after all deferred scripts have executed, guaranteeing all FC.* constants exist before app.js runs.
Backpressure — before sending each 64 KB chunk, app.js checks dc.bufferedAmount. If it exceeds FC.BUFFER_HIGH (2 MB), sending pauses until the bufferedamountlow event fires. This prevents out-of-memory errors on the sender for very large files.
ICE timer placement — the relay hint timer starts when a connection attempt is made (not when the connection opens). This is critical: if ICE fails, the open event never fires, so the timer must be started before it.
Zombie connection guard — when a second connection attempt arrives at the sender, the app checks conn.open (not just conn). A failed ICE attempt leaves a zombie conn object with open === false. Checking conn.open allows the sender to accept a fresh retry from the receiver without a page refresh.
- Fork this repository to your GitHub account
- On Netlify: Add new site → Import an existing project → GitHub
- Select the repo — no build command needed, publish directory is
.(root) - Deploy — Netlify provides HTTPS automatically
- Every
git pushtomaintriggers a redeployment
The deployed site uses the PeerJS public cloud (0.peerjs.com) — no setup needed. FC.PEER_HOST is set to '' in config.js, which tells the app to use it automatically.
The peerserver/ directory contains a minimal Node.js PeerJS server if you want to self-host (private deployment, guaranteed uptime, no dependency on the public cloud):
- On Render.com: New → Web Service
- Connect your GitHub repo; set Root Directory to
peerserver - Build command:
npm install— Start command:npm start - After deploy, you'll get a URL like
https://your-app.onrender.com - In
assets/js/config.js, set:FC.PEER_HOST = 'your-app.onrender.com';
Note: Render's free tier spins down after 15 minutes of inactivity — first connection after a cold start can take 20–30 seconds. Only use a custom host if it's always-on (paid tier or self-managed VPS).
All constants live in assets/js/config.js and are documented inline:
/* ── Signaling server ──────────────────────────────────────── */
FC.PEER_HOST = ''; // '' = PeerJS public cloud; set to your Render URL to self-host
FC.PEER_PORT = 443;
FC.PEER_PATH = '/peerjs';
FC.PEER_SECURE = true;
FC.PEER_DEBUG = 0; // 0 = silent | 1 = errors | 2 = warnings | 3 = verbose
/* ── Transfer ──────────────────────────────────────────────── */
FC.CHUNK_SIZE = 65536; // 64 KB per DataChannel send
FC.BUFFER_HIGH = 2 * 1024 * 1024; // Pause sending above 2 MB buffered
FC.FALLBACK_MAX = 512 * 1024 * 1024; // In-memory Blob cap (non-Chrome browsers)
/* ── Timeouts ──────────────────────────────────────────────── */
FC.ICE_RELAY_HINT_MS = 8000; // Show relay hint after 8 s of failed ICE
FC.PEER_RECONNECT_MS = 2000; // Delay before signaling-server reconnect
FC.CANCEL_FLUSH_MS = 400; // Wait before force-closing after cancelThe default TURN configuration in config.js uses two free public relay providers:
| Provider | Servers | Notes |
|---|---|---|
openrelay.metered.ca |
UDP/80, UDP/443, TCP/80, TCP/443, TLS/443 | Globally distributed |
freeturn.net |
UDP/3478, TLS/5349 | Secondary fallback |
These are sufficient for the vast majority of users. If you need guaranteed relay for networks where these providers are blocked (see Known limitation):
Option A — Metered.ca free tier (50 GB relay/month)
- Sign up at metered.ca — free account
- Create a project → copy your ICE credentials from the dashboard
- Replace
FC.ICEinconfig.jswith your credentials
Security note: Static TURN credentials in client-side JavaScript are readable by anyone who opens DevTools. For a public production app with many users, generate short-lived credentials server-side (e.g. a Netlify Function that calls the Metered.ca REST API) so the real API key is never shipped to the browser. Each user gets a 1-hour credential that expires after their session.
All UI strings used by app.js are in assets/js/i18n.js. The site is English-only. The FC.t('key') API is used throughout app.js to look up strings — edit i18n.js to change any displayed text.
| Detail | |
|---|---|
| File data | Never leaves the two browsers — pure P2P |
| Signaling metadata | Peer IDs and ICE candidates only; never stored, discarded after connection |
| Relay traffic | Passes through TURN server encrypted (DTLS) only if direct P2P fails |
| Analytics | None |
| Cookies | None |
| Accounts | None required |
MIT — free to use, modify, and self-host.