June 20, 2026
Building DropWire: local file transfer from a browser, no accounts required
I built DropWire because I wanted one simple thing: move a file between my Mac and my android phone without installing an app, logging into…
AroicX
5 min read
I built DropWire because I wanted one simple thing: move a file between my Mac and my android phone without installing an app, logging into an account, uploading to cloud storage, or sending the file through a messaging app.
That sounds small, but the experience matters. The moment you need to move a video from an Android phone to a Mac, or a PDF from a laptop to an iPhone, the usual options get weirdly heavy. Cloud drives need accounts. Messaging apps compress or re-host files. Cables are inconsistent. AirDrop is excellent, but only if every device is inside Apple's ecosystem.
DropWire is my attempt at the lightest possible cross-device version of that flow.
What DropWire does
DropWire lets nearby devices transfer files through the browser.
- Creating a room with a short room code
- Joining from another device with a QR code or room code
- Seeing connected devices in real time
- Sending multiple files
- Accepting or declining incoming transfers
- Tracking per-file progress
- Keeping local browser history with IndexedDB
- Finding currently online DropWire devices with Radar
- Running the whole stack locally from a single CLI command
The important constraint is that DropWire does not store files on the server. The server only coordinates devices. File bytes move directly between browsers.
Why not just upload to a server?
Uploading to a server would have been easier, but it would have changed the product. Once files pass through a backend, the app needs answers for storage, retention, deletion, authentication, abuse prevention, privacy, bandwidth cost, and infrastructure scaling. Those are real concerns, but they are not the MVP I wanted.
The goal was simpler: If two devices are nearby and both browsers are open, they should be able to move a file directly. That pushed the architecture toward WebRTC.
The architecture
DropWire has two main pieces:
- A Vue 3 web app
- A Go WebSocket signaling server
The signaling server handles:
- Device presence
- Room creation
- Room joining
- Radar invites
- WebRTC offer/answer/ICE message relay
The browser handles:
- File selection
- Transfer acceptance
- WebRTC peer connection setup
- DataChannel file transfer
- Progress tracking
- Downloading received files
- IndexedDB history
The file transfer path is intentionally direct:
Browser A ← — WebRTC DataChannel — → Browser BBrowser A ← — WebRTC DataChannel — → Browser BThe server sits to the side:
Browser A ← — WebSocket signaling — → Go server ← — WebSocket signaling — → Browser BBrowser A ← — WebSocket signaling — → Go server ← — WebSocket signaling — → Browser BThat separation is the core technical decision in DropWire.
How the user flow works
- Find devices with Radar
Radar shows devices that currently have DropWire open and connected to the same local signaling server. One device can click Connect, and the other device receives a prompt to accept.
There is an important browser limitation here: a closed browser cannot receive a modal prompt. To support that, DropWire would need PWA push notifications or a native app. For the MVP, Radar is intentionally scoped to online browser sessions.
- Create or join a room
Rooms are held in memory by the Go server. A room has a short code and supports up to five connected devices. Users can join by typing the code or scanning the QR code.
For local phone testing, QR codes use the Mac's LAN IP instead of localhost, so a phone can open the correct URL from the same Wi-Fi network.
- Send files directly
The sender selects one or more files. The receiver gets an incoming transfer prompt. After acceptance, DropWire opens a WebRTC DataChannel and streams the selected files in chunks.
Each file has its own progress state, so a batch transfer feels understandable instead of becoming one anonymous loading bar.
Why Vue, Go, WebSocket, and WebRTC?
I chose Vue 3 and Vite because the frontend needed to stay small and fast to iterate. The UI is mostly state transitions: devices appear, rooms update, files enter queues, transfers progress, and history changes. Vue's composition model fit that nicely. The most important technical decision was to keep file bytes out of the server. That made the backend a coordinator instead of a storage system, which kept the privacy model easier to explain and avoided building retention, cleanup, and bandwidth infrastructure before the product needed it.
I also split the communication layer in two: WebSocket for control messages, WebRTC DataChannel for the actual transfer. WebSocket is predictable for room presence and signaling, while WebRTC is better suited for direct browser-to-browser data once both peers agree to connect. The multi-device change forced another architecture decision: every device pair needed its own peer session. A single global DataChannel works for a demo, but it breaks down as soon as any connected device should be able to send or receive. Tracking peer sessions by device id made the room feel symmetrical instead of Mac-first.
I chose Go for the signaling server because this part of the app benefits from boring reliability. The server is in-memory, concurrent, and WebSocket-heavy. Go is a natural fit for that shape.
I used WebSocket only for signaling. This includes:
- Registering devices
- Broadcasting room updates
- Sending WebRTC offers
- Sending WebRTC answers
- Relaying ICE candidates
- Sending file request metadata
- Sending accept/decline responses
I used WebRTC DataChannel for file bytes because it lets the transfer happen directly between browsers after negotiation.
What got tricky
Mobile browser security context
Something worked on localhost but failed on phones: file selection.
The issue was crypto.randomUUID(). On the Mac, the app was running on localhost, which browsers treat as a secure context. On a phone visiting [http://192.168.x.x](http://192.168.x.x`), some APIs are not always available. The fix was to add a cross-browser ID helper with a fallback.That was a good reminder that local network web apps do not behave exactly like localhost.
IPhone signaling
The app originally relied on Vite's WebSocket proxy. Desktop browsers were fine, but phones can be less forgiving. I changed the config endpoint so the web app receives a direct signaling URL:
{
"lanOrigin": "[http://0.0.0.0:5173](http://192.168.1.64:5173)",
"wsUrl": "ws://0.0.0.0:8080/ws"
}{
"lanOrigin": "[http://0.0.0.0:5173](http://192.168.1.64:5173)",
"wsUrl": "ws://0.0.0.0:8080/ws"
}Now the web app can load from Vite while connecting directly to the Go signaling server.
Per-peer transfer state
The first version had one global peer connection and one global DataChannel. That worked for the simplest Mac-to-phone flow, but it was not a real multi-device architecture.
The fix was to track peer sessions by device id:
peerSessions: Map<deviceId, PeerSession>peerSessions: Map<deviceId, PeerSession>Each peer gets its own:
- RTCPeerConnection
- DataChannel
- Send queue
- Receive state
That makes every connected device capable of sending and receiving.
The CLI
I also wanted DropWire to feel easy to run. So I added a local CLI:
dropwiredropwireIt starts both the Go server and the Vite app and shows a live terminal dashboard with URLs, connected devices, rooms, messages, invites, and recent logs.
That turned out to be more than polish. When building local-network software, observability matters. You need to know which URL the phone should open, whether the WebSocket is live, and whether devices are actually registering.
What DropWire is not
DropWire is not trying to be cloud storage.
What I learned
The biggest lesson is that simple user experiences often require strict architecture boundaries.
For DropWire, the boundary is:
- The server coordinates.
- The browser transfers.
- The server never stores files.
That one decision shaped everything else.
It kept the backend small. It made privacy easier to reason about. It exposed the browser limitations early. And it gave the product a clear identity: local, lightweight, direct.
Open browser. Find device. Accept. Send file. Done.
That is the feeling I wanted.
Feel free to connect with me on:
X, Linkedin also you can contriubte to the repo https://github.com/AroicX/dropwire