NearCast — How It Works
End-to-end guide to the P2P file sharing pipeline
Introduction
NearCast is a browser-native, P2P file sharing application built on WebRTC DataChannel — the same open standard that powers real-time communication in modern browsers. No account, no plugin, no download. Files travel directly between devices without ever touching a server.
This page explains how NearCast works under the hood — from the moment a user creates a room to the point where file bytes are flowing directly peer-to-peer. It covers the WebRTC handshake, the two signaling modes, the chunked file transfer engine, and the screen state machine. Each section builds on the last.
High-Level Architecture
Device A
Initiator
Pusher Channel
nearcast-{code} · /api/nearcast/signal
STUN Server
stun.l.google.com
Device B
Receiver
Once connected, all file bytes flow directly peer-to-peer — the signal server receives no file data.
The Pusher channel — reached via a Next.js API route at /api/nearcast/signal — acts only as a temporary message bus during the WebRTC handshake. It carries SDP offers, SDP answers, and ICE candidates between the two devices. A Google STUN server helps each device discover its public address for NAT traversal.
Once the handshake completes and the RTCDataChannel opens, all file bytes flow directly between the two browsers. The Pusher channel receives no file data and no further traffic for that session.
WebRTC Data Channel
NearCast uses RTCDataChannel rather than media tracks. Unlike audio/video streams, a DataChannel carries arbitrary binary or text messages — making it ideal for file transfer. The channel is created with { ordered: true } so chunks arrive in the exact sequence they were sent, eliminating any need for reordering logic on the receiver.
The initiator calls pc.createDataChannel() before generating the SDP offer. The receiver discovers the channel via the ondatachannel event fired on its RTCPeerConnection. Both sides wait for the channel's onopen event before attempting any file transfer — this is the signal that the P2P path is live.
Chunks guaranteed to arrive in send order — no sequence numbers needed in the application layer.
Each chunk is an ArrayBuffer — zero encoding overhead compared to base64 strings.
Sender checks bufferedAmount before each chunk to avoid overwhelming the DataChannel buffer.
Signaling Flow
- 1Both devices subscribe to the Pusher channel nearcast-{code}. The room code was previously created via POST /api/nearcast/room and stored in Upstash Redis.
- 2The receiver's Pusher subscription triggers a subscription_succeeded event. At that point — and only then — it posts a READY signal to guarantee it is listening before any offer is sent.
- 3The initiator receives the READY signal and creates the DataChannel. It then calls createOffer() and setLocalDescription(offer).
- 4The initiator posts the SDP offer as a JSON payload to /api/nearcast/signal, which relays it to the Pusher channel.
- 5The receiver receives the offer, calls setRemoteDescription(offer), generates an answer with createAnswer(), and stores it via setLocalDescription(answer).
- 6The receiver posts the SDP answer back through /api/nearcast/signal.
- 7The initiator receives the answer and applies it with setRemoteDescription(answer). Both sides now agree on codecs and network capabilities.
- 8As each device collects ICE candidates, it posts them through the same signal route. The other device calls addIceCandidate() on each one.
- 9A viable ICE candidate pair is found, connectionState transitions to connected, and the DataChannel's onopen event fires. Pusher receives no further traffic for this session.
Connection Modes
Auto
Recommended- –Share a 6-character room code or QR — peer enters it to join
- –Pusher channel nearcast-{code} relays SDP + ICE in real time
- –Receiver sends READY on subscribe; initiator creates offer in response
- –Requires Pusher credentials (NEXT_PUBLIC_PUSHER_KEY, etc.)
- –Handshake typically completes in under 2 seconds on modern networks
Manual
Fully offline- –Initiator's SDP offer is base64-encoded and rendered as a QR code
- –Receiver scans or pastes the base64 string — no internet required
- –Receiver's answer is similarly base64-encoded and shared back
- –ICE candidates are bundled into the SDP (trickle-ICE disabled)
- –Ideal for air-gapped environments or when Pusher is unavailable
Once the WebRTC DataChannel opens, both modes are identical — all file bytes travel directly peer-to-peer. The signaling transport only affects how long the handshake takes, not what happens after.
Both modes produce the same WebRTC DataChannel — only the signaling transport differs. Auto mode is the right choice for everyday use; Manual mode exists for environments where Pusher is unavailable or a fully serverless, offline handshake is required.
File Transfer Engine
NearCast splits each file into 64 KB chunks before sending. Each chunk message carries a structured header: { fileId, chunkIndex, totalChunks, payload }". The receiver acknowledges every chunk; the sender tracks unacknowledged chunks and can replay from any checkpoint.
File.slice(offset, offset + CHUNK_SIZE)Files are read with the File API and sliced into 64 KB ArrayBuffer segments. The chunk size balances DataChannel buffer limits with transfer efficiency.
{ type: 'ack', fileId, chunkIndex }Receiver sends an ACK message after each chunk is written to the Dexie table. The sender only advances to the next chunk after receiving the ACK.
{ type: 'resume', fileId, fromChunk: N }On reconnect, the receiver checks its Dexie transfer state and sends a RESUME message with the last successful chunk index. The sender replays from that point.
new Blob(chunks, { type: file.type })Once all chunks are received and ACKed, the Dexie records are read in order, assembled into a Blob, and handed to the browser's download mechanism.
Transfer speed is sampled live over a rolling window. The useSpeedMeter hook reads bytes acknowledged per second and maps the result to a badge: Fast (WiFi/LAN), Medium (4G), or Slow (3G or worse).
Screen Flow
From room or error: disconnect / retry returns to landing.
connecting covers both connecting-auto and connecting-manual.
| Screen | Meaning | Triggered by |
|---|---|---|
landing | Home — mode selector, user ID chip, join by room code | Initial load or disconnect / retry |
connecting-auto | Waiting for peer via Pusher; shows room code + QR | Auto mode selected; room created or code entered |
connecting-manual | QR offer display and answer paste field | Manual mode selected |
room | DataChannel open; drag-drop zone, file queue, progress | RTCPeerConnection reaches connected state |
error | WebRTC handshake failed or DataChannel closed unexpectedly | connectionState → failed / closed / disconnected |
Limits & Infrastructure
| Item | Limit / Detail | Enforcement |
|---|---|---|
| Room code TTL | Upstash Redis key expiry | SET nearcast:{code} EX 3600 — room codes expire after 1 hour of inactivity |
| Pusher messages | 200k / day (free tier) | Signaling only — each handshake uses ~10–20 messages; no file data passes through |
| Pusher connections | 100 simultaneous (free tier) | Each NearCast room uses 2 connections; supports ~50 concurrent sessions |
| File size | No server-side limit | Only browser memory and DataChannel buffer constrain transfer size |
| IndexedDB transfer state | Persisted per fileId | Dexie nearcast_transfers table; cleaned up when transfer completes or user clears storage |
Because file bytes never pass through any server, NearCast's infrastructure cost scales with the number of connections established, not with the volume of data transferred. A 10 GB file transfer costs exactly the same in Pusher messages as a 10 KB one — roughly 15–20 signals per session regardless of file size.