Zap is a no-signup file transfer tool with two modes:
| Mode | How it works |
|---|---|
| Direct P2P | Files go browser → browser over a WebRTC DataChannel. The WebSocket (via ZapHub Durable Object) is used only for signaling (offer/answer/ICE). If a direct path can't be established, the Durable Object relays the binary data as a fallback. Nothing is stored on any server. |
| Cloud Hold | Files are AES-GCM encrypted in 5 MB chunks and uploaded to Cloudflare R2 inside a temporary room. Downloads stream through the Worker for decryption with accurate progress. The room auto-deletes in 1–24 hours. |
- ⚡ Instant rooms — 6-character code, no sign-up, no dashboard
- 🔒 End-to-end encrypted — AES-GCM (Web Crypto API) for Cloud Hold; direct channel for P2P
- 🌐 WebRTC P2P — files sent over
RTCDataChannelin 256 KB chunks, with ICE restart on failure - 🔁 Server relay fallback —
ZapHubrelays binary chunks when direct ICE path fails - ☁️ Cloud Hold — R2-backed ephemeral rooms, multipart upload support, streamed downloads
- 📶 Connection quality badge — shows Direct/Relayed and round-trip time in real time
- 🧹 Auto-purge — cron trigger every 10 min deletes expired rooms and R2 objects
- 📱 QR sharing — shareable link with QR code for P2P sessions
- 🗂️ Active rooms panel — browser-local session cache with live expiry countdowns
Zap/
├── frontend/ → Static assets served via Cloudflare Workers Assets
│ ├── index.html → Single-page app (SPA, no framework)
│ ├── style.css → All styles (vanilla CSS, dark/light themes)
│ ├── script.js → All app logic: P2P, Cloud Hold, Encryption, UI
│ └── assets/
│ └── zap logo.svg → Logo + favicon
└── backend/
├── src/
│ ├── index.js → Worker entry point — API routing, R2 proxy, cron cleanup
│ └── ZapHub.js → Durable Object — WebSocket hub, signaling, server relay
└── wrangler.toml → Deployment config (gitignored — copy from wrangler.toml.example)
Sender ─── WebSocket ──► ZapHub DO ◄── WebSocket ─── Receiver
↕ signaling (offer / answer / ICE)
Sender ─────────────── RTCDataChannel ───────────────► Receiver
(256 KB binary chunks, direct or relayed)
- Both peers connect to
ZapHubover WebSocket - Sender creates an
RTCPeerConnection→ sends SDP offer via WebSocket - Receiver answers → ICE negotiation completes
- File bytes flow over the
RTCDataChannel(256 KB chunks) - If ICE fails,
ZapHubrelays rawArrayBufferchunks as fallback
Browser
↓ AES-GCM encrypt (5 MB chunks, random IV per chunk)
Worker API → Cloudflare R2 (via Worker proxy or multipart PUT)
↓
Receiver → Worker streaming download → chunked AES-GCM decrypt → save
| Service | Purpose |
|---|---|
| Workers | API layer, R2 upload/download proxy, cron cleanup |
Durable Objects (ZapHub) |
WebSocket signaling + server-side binary relay fallback |
| R2 | Encrypted file storage for Cloud Hold rooms |
KV (ROOMS) |
Cloud Hold room metadata + expiry |
KV (ROOMS_KV) |
R2 storage usage counter |
| Workers Assets | Serves the frontend/ static files |
| Cron Triggers | */10 * * * * — cleanup expired rooms and orphaned R2 objects |
- Node.js ≥ 18
- A Cloudflare account with Workers, R2, and KV enabled
git clone https://github.com/RitwikRishRaj/Zap.git
cd Zap/backendcp wrangler.toml.example wrangler.tomlFill in your real IDs in wrangler.toml:
[[kv_namespaces]]
binding = "ROOMS_KV"
id = "<your-kv-namespace-id>"
[[kv_namespaces]]
binding = "ROOMS"
id = "<your-rooms-kv-namespace-id>"
[[r2_buckets]]
binding = "FILES"
bucket_name = "<your-r2-bucket-name>"npm install
npm run deploynpm run dev
# → http://localhost:8787Set in wrangler.toml under [vars]:
| Variable | Default | Description |
|---|---|---|
MAX_ROOMS_PER_IP |
10 |
Max active P2P/DO rooms per IP |
DEFAULT_ROOM_TTL_HOURS |
24 |
Default P2P room lifetime |
MAX_R2_STORAGE_GB |
8 |
Total R2 storage cap |
MAX_R2_FILE_SIZE_GB |
2 |
Max single file size |
MAX_ROOM_FILES |
10 |
Max files per Cloud Hold room |
MAX_CONCURRENT_RELAYS |
3 |
Max simultaneous server relay sessions |
RATE_LIMIT_WINDOW_MS |
900000 |
Rate limit window (15 min) |
RATE_LIMIT_MAX_REQUESTS |
100 |
Max requests per window |
Optional — Cloudflare TURN (improves P2P behind strict NAT):
[vars]
CF_TURN_KEY_ID = "..." # from dash.cloudflare.com > Calls
CF_TURN_API_TOKEN = "..."Without TURN keys, Zap falls back to stun:stun.cloudflare.com:3478 and the server relay.
| Method | Path | Description |
|---|---|---|
GET |
/api/health |
Health check + version |
WS |
/ws |
WebSocket — P2P signaling & relay (ZapHub DO) |
POST |
/api/room/create |
Create a Cloud Hold room |
GET |
/api/room/:code |
Get room info & file list |
DELETE |
/api/room/:code |
Delete room (requires Authorization: Bearer <adminToken>) |
POST |
/api/room/upload |
Initiate file upload (returns upload URL + fileId) |
PUT |
/api/room/upload-file/:code/:fileId/:name |
Direct Worker upload fallback |
POST |
/api/room/upload-complete |
Finalize upload / complete multipart |
GET |
/api/room/:code/download/:fileId |
Stream-download a file (sets Content-Length for progress) |
GET |
/api/storage-stats |
R2 usage stats |
GET |
/api/turn-credentials |
ICE server config (STUN/TURN) for WebRTC |
GET |
/api/encryption-key |
AES-GCM key hex (derived from ENCRYPTION_PASSWORD) |
GDG On Campus · KIIT · Cloud Team, Bhubaneswar — Google Developer Groups student community.
MIT © 2026 Ritwik