AirDrop for the browser — send text, images and files to devices on your LAN from a browser extension, routed peer-to-peer by a tiny local Go daemon. No cloud, no account, no upload.
+-----------------------------------------------------------+
| Browser (Chrome / Brave / Firefox) |
| |
| +--------------+ +-----------------------------+ |
| | Popup | <----> | Background service worker | |
| | (React UI) | msg | owns the WebSocket | |
| +--------------+ +--------------+--------------+ |
+----------------------------------------------|-------------+
|
HTTP GET /info /health | ws://127.0.0.1:8765/ws
(loopback only) |
v
+-----------------------------------------------------------+
| share-daemon (Go, binds 127.0.0.1:8765) |
| |
| HTTP API | WebSocket hub | device identity/config |
| mDNS advertise + browse (_sharing._tcp . local.) |
| WebRTC signaling relay (offer / answer / ICE) |
+-------------------------------|---------------------------+
|
mDNS on the local network
|
v
+-------------------------------+
| Peer device running the same |
| daemon + extension |
+---------------|---------------+
|
WebRTC DataChannel (DTLS-encrypted)
LAN direct -> STUN -> TURN (fallback)
|
v
bytes move device <-> device
The daemon exists because a browser extension cannot do mDNS, UDP broadcast, raw sockets, or bind to
arbitrary network interfaces. See docs/ARCHITECTURE.md.
- Local-network device discovery over mDNS (
_sharing._tcp) — planned, M4 - Peer-to-peer transfer over a WebRTC DataChannel — planned, M7
- Send text / links, then images, files and video — planned, M9+
- Cross-browser: Chrome, Brave and Firefox from one WXT codebase
- Cross-platform daemon: Windows, macOS, Linux
- Loopback-only daemon bind — nothing listens on your LAN interface except mDNS
- No cloud relay, no account, no telemetry
| Milestone | Scope | Status |
|---|---|---|
| M1 | Repo scaffolding, protocol spec, extension shell + popup UI | Done |
| M2 | Go daemon skeleton: HTTP /info + /health, WebSocket /ws, device identity, CORS/PNA |
Done |
| M3 | Extension <-> daemon live connection, reconnect backoff, real state in popup | Pending |
| M4 | mDNS advertise + browse, real device list | Pending |
| M5 | Device list push to the extension (devices message) |
Pending |
| M6 | Pairing and trust (6-digit code, persisted peers) | Pending |
| M7 | WebRTC signaling relay + DataChannel establishment | Pending |
| M8 | Send text end-to-end between two machines | Pending |
Everything past M2 is not implemented. With no daemon running (the normal case today) the popup
reports disconnected and shows a hardcoded placeholder device list — that is intended M1 behaviour.
Full roadmap through M15: docs/PLAN.md.
cd daemon
go run ./cmd/daemonIt binds 127.0.0.1:8765. Verify:
curl http://127.0.0.1:8765/info
curl http://127.0.0.1:8765/healthcd extension
npm install
npm run devFor Firefox:
npm run dev:firefoxnpm run dev launches a browser with the extension already loaded. To load a build by hand:
Chrome / Brave
- Open
chrome://extensions - Enable Developer mode (top right)
- Load unpacked -> select
extension/.output/chrome-mv3
Firefox
- Open
about:debugging#/runtime/this-firefox - Load Temporary Add-on...
- Select
extension/.output/firefox-mv3/manifest.json
sharing/
├── extension/ WXT + React + TypeScript browser extension
│ ├── entrypoints/
│ │ ├── background.ts owns the WebSocket to the daemon
│ │ └── popup/ React UI
│ ├── types/index.ts shared TS types (mirrors shared/PROTOCOL.md)
│ └── wxt.config.ts
├── daemon/ Go local daemon (share-daemon)
│ ├── cmd/daemon/ main
│ ├── protocol/ wire types, mirrors shared/PROTOCOL.md 1:1
│ ├── server/ HTTP + WebSocket
│ └── config/ device identity, config.json
├── shared/
│ └── PROTOCOL.md AUTHORITATIVE wire contract — both sides follow it
├── docs/
│ ├── PLAN.md roadmap, milestones, test matrix, packaging
│ └── ARCHITECTURE.md why a daemon, responsibility split, lifecycle
├── signaling/ placeholder for the optional M15 relay (not built)
├── docker-compose.yml commented-out placeholder for that relay
├── .editorconfig
├── .gitattributes
└── .gitignore
- Loopback-only bind. The daemon's HTTP + WebSocket listener binds
127.0.0.1, never0.0.0.0. Nothing on your LAN can reach the control plane. - Origin allowlist + Private Network Access. Only
chrome-extension://,moz-extension://,safari-web-extension://andhttp://localhost:<port>/http://127.0.0.1:<port>origins are echoed back inAccess-Control-Allow-Origin. Chrome's PNA preflight for127.0.0.1is answered explicitly. - No cloud relay. Bytes go device to device on your LAN. A TURN relay is a last-resort fallback and is not part of the MVP; there is no server that ever stores your files.
- WebRTC encryption. DataChannels are DTLS-encrypted end to end (mandatory in WebRTC, not optional).
- Device trust. Each device has a persisted RFC 4122 v4 UUID. Pairing (M6) uses a 6-digit code confirmed on both ends; unpaired devices cannot open a transfer.
- Local identity file.
os.UserConfigDir()/sharing/config.json, mode0600, dir0700.
shared/PROTOCOL.md— the authoritative wire protocol. TypeScript and Go must match it field-for-field.docs/PLAN.md— milestones, week-one schedule, browser/OS test matrix, packaging.docs/ARCHITECTURE.md— design rationale and connection lifecycle.