NearCast — How It Works

End-to-end guide to the P2P file sharing pipeline

← Back to app

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

SDP / ICE signals

Pusher Channel

nearcast-{code} · /api/nearcast/signal

Public IP discovery

STUN Server

stun.l.google.com

SDP / ICE signals

Device B

Receiver

↔ P2P DataChannel (file chunks)

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.

Ordered delivery

Chunks guaranteed to arrive in send order — no sequence numbers needed in the application layer.

Binary messages

Each chunk is an ArrayBuffer — zero encoding overhead compared to base64 strings.

Backpressure-aware

Sender checks bufferedAmount before each chunk to avoid overwhelming the DataChannel buffer.

Signaling Flow

Initiator
Receiver
Subscribe to Pusher nearcast-{code}
1
Subscribe to Pusher nearcast-{code}
·
2
subscription_succeeded → POST { type: 'ready' }
Receives READY → createDataChannel() + createOffer()
3
·
POST /api/nearcast/signal { type: 'offer' }
4
·
·
5
setRemoteDescription(offer) + createAnswer()
·
6
POST /api/nearcast/signal { type: 'answer' }
setRemoteDescription(answer)
7
·
ICE candidates exchanged via Pusher (both sides)
8
ICE candidates exchanged via Pusher (both sides)
connectionState → connected · DataChannel opens
9
connectionState → connected · DataChannel opens
  1. 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.
  2. 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.
  3. 3The initiator receives the READY signal and creates the DataChannel. It then calls createOffer() and setLocalDescription(offer).
  4. 4The initiator posts the SDP offer as a JSON payload to /api/nearcast/signal, which relays it to the Pusher channel.
  5. 5The receiver receives the offer, calls setRemoteDescription(offer), generates an answer with createAnswer(), and stores it via setLocalDescription(answer).
  6. 6The receiver posts the SDP answer back through /api/nearcast/signal.
  7. 7The initiator receives the answer and applies it with setRemoteDescription(answer). Both sides now agree on codecs and network capabilities.
  8. 8As each device collects ICE candidates, it posts them through the same signal route. The other device calls addIceCandidate() on each one.
  9. 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.

ChunkingFile.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.

ACK protocol{ 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.

Resume on failure{ 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.

Final assemblynew 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

landing
mode selected
connecting
peer joins
room
also fromconnecting
WebRTC failed
error

From room or error: disconnect / retry returns to landing.
connecting covers both connecting-auto and connecting-manual.

ScreenMeaningTriggered by
landingHome — mode selector, user ID chip, join by room codeInitial load or disconnect / retry
connecting-autoWaiting for peer via Pusher; shows room code + QRAuto mode selected; room created or code entered
connecting-manualQR offer display and answer paste fieldManual mode selected
roomDataChannel open; drag-drop zone, file queue, progressRTCPeerConnection reaches connected state
errorWebRTC handshake failed or DataChannel closed unexpectedlyconnectionState → failed / closed / disconnected

Limits & Infrastructure

ItemLimit / DetailEnforcement
Room code TTLUpstash Redis key expirySET nearcast:{code} EX 3600 — room codes expire after 1 hour of inactivity
Pusher messages200k / day (free tier)Signaling only — each handshake uses ~10–20 messages; no file data passes through
Pusher connections100 simultaneous (free tier)Each NearCast room uses 2 connections; supports ~50 concurrent sessions
File sizeNo server-side limitOnly browser memory and DataChannel buffer constrain transfer size
IndexedDB transfer statePersisted per fileIdDexie 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.

Built with Claude CodeAnthropic's AI coding assistant. Speed up your dev workflow today!