Move files between your phone and your laptop — over Wi-Fi or a USB cable. No cloud, no account, no upload limits.
Browsing a connected phone and copying files to the Mac — the whole round trip.
Getting a file off an Android phone onto a Mac is genuinely annoying. Google's Android File Transfer was discontinued, AirDrop doesn't speak to Android, and everything else wants you to upload a private file to somebody's server and download it again — slow, and a strange thing to do with your own photos.
Beam does the obvious thing instead: your phone and your laptop are already on the same Wi-Fi, so they should just talk to each other. And when they're joined by a cable, that should work too.
One file transfers in the time it takes a cloud app to finish uploading.
| 📶 Wi-Fi transfer, both directions | Phone → laptop and laptop → phone, over your local network. Devices find each other automatically. |
| 🔌 USB cable mode | Browse a connected Android like a drive and copy files either way. |
| 🗂 Manage the phone's files | Create folders, rename, move, delete — from your laptop. |
| 🖱 Real drag and drop | Drag files out of the phone into Finder, and drop files or whole folders from Finder onto the phone. |
| ⏱ Remembers your devices | A device you've used before comes back in about a second, and there's a history of what you've sent and received. |
| 🖼 Browse by kind | Images, Videos, Audio, Documents, Archives — with real previews, including video frames and PDF first pages. |
| 📷 The whole phone at once | A cabled phone is indexed from its own MediaStore in about a second, so Camera, Videos and Images span every folder — sort by size and the 7 GB clip is the first row. |
| 🍎 Works with iPhone too | Over Wi-Fi. iOS gives no USB file access to anyone, so cable mode is Android-only. |
| 🔒 Nothing leaves your network | No server, no account, no telemetry. |
Devices on the left, an explorer on the right. Everything that's reachable appears in one place: this Mac, phones on USB, and phones on Wi-Fi.
Phones that are plugged in but not usable don't silently disappear — they're listed with the actual reason and what to do about it (that greyed-out Samsung is a macOS restriction explained below).
Left: Android has found both the MacBook and the iPhone on the network. Right: iOS in receive mode, showing files as they arrive.
Every device speaks the same tiny HTTP protocol. There is no central server — each device is both a client and a server.
graph LR
subgraph "Your Wi-Fi"
M["💻 Mac<br/>Electron app<br/>:8790"]
A["🤖 Android<br/>React Native<br/>:8791"]
I["📱 iPhone<br/>React Native<br/>:8791"]
end
M <-->|"GET /info<br/>POST /upload"| A
M <-->|"GET /info<br/>POST /upload"| I
A <-->|"GET /info<br/>POST /upload"| I
M -.->|"adb: browse, pull,<br/>push, mkdir, mv, rm"| A
The protocol:
GET /info→{"app":"beam","name":"Vickys-MacBook","platform":"darwin","features":["offer"]}POST /offer→ declares who is asking and what they want to sendGET /offer/<id>→ the sender polls until the other side answersPOST /upload?token=<token>→ the bytes, once permission exists
A file-transfer tool that accepts anything from anyone on the network is a liability, so the receiver asks first:
- The sender offers — its name, and the list of files with sizes.
- The receiver prompts, showing exactly what is about to arrive and a six-digit code. The sending device shows the same code, so you can tell which device is really asking — names alone are trivially spoofable on a LAN.
- Only after you accept does the sender get a one-use token, valid for exactly as many files as it declared. No token, no transfer.
Tick Always allow this device and that device skips the prompt next time; trust is keyed to a per-install device id, stored locally.
All three receivers do this — the Mac shows a native dialog, the phones show a sheet with the file list and the code.
Senders running an older build can't make an offer, so the receiver prompts when their upload arrives instead. On the Mac and on Android the request body is left unread until you decide, so declining costs no bandwidth and writes nothing. On iOS the framework hands the handler an already-parsed body, so the bytes have arrived by then — nothing is saved unless you accept, and the temporary files are deleted if you decline.
This is authorisation, not authentication. It stops silent drive-by transfers, which is the realistic risk on a home network. It is not a defence against an attacker who is already on your Wi-Fi and actively spoofing — that needs TLS, which is the next thing on the list.
Discovery is a subnet sweep, deliberately. A device reads its own Wi-Fi IP,
then probes /info on all 254 addresses of its /24 in parallel. The obvious
alternative — mDNS/Bonjour — needs a different native library on every platform,
behaves differently on each, and is exactly the kind of dependency that breaks
six months later. A parallel HTTP sweep is a few dozen lines, has no
dependencies, and behaves identically on macOS, Android and iOS. It finishes in
about a second.
Ports carry meaning: 8790 = laptops, 8791 = phones. A scanner that finds
8791 knows it found a phone before it parses anything.
beam/
├── desktop/ Electron app (the Mac side)
│ └── src/
│ ├── main.js receiver, IPC, native drag, confirm dialogs
│ ├── cable.js USB: adb + libmtp backends, file operations
│ ├── wifi-send.js discovery sweep + upload
│ └── app.js the two-pane UI
└── mobile/ React Native app (Android + iOS)
├── src/ discovery, upload, receiver bridge
└── android|ios/ native receive-mode modules
The phone's receiver is native on both platforms — NanoHTTPD (Kotlin) on Android, GCDWebServer (Objective-C) on iOS — bridged to one shared JS API, so the React Native side stays identical across platforms.
Download the latest release →
— a macOS .dmg and a signed Android .apk, with SHA-256 checksums in the
release notes.
The Mac build isn't code-signed (that needs a paid Apple Developer ID), so the first launch needs right-click → Open. Android will ask you to allow installing outside the Play Store.
cd desktop && npm install && npm run distThe dmg lands in desktop/release/. Or run it from source with npm start.
cd mobile && npm install
npx react-native start # Metro, in one terminal
npx react-native run-android # in anotherFor iOS, install pods first — CocoaPods needs a UTF-8 locale or it crashes:
cd mobile/ios && LANG=en_US.UTF-8 pod install && cd .. && npx react-native run-iosOver Wi-Fi: open Beam on both devices, turn on Receive files on the phone, and each shows up in the other's list. Pick files and send, or drag files onto the laptop's drop zone.
Over a cable: connect an Android with USB debugging enabled. It appears under USB, and the right pane becomes a file browser: tick files and Copy to …, drag them into Finder, or drop files in to copy them the other way. Copies land in the Beam folder unless you pick somewhere else with the ⋯ button next to it, and Finder opens on what arrived so you never have to guess where it went.
Dragging a file off the phone takes two drags. Finder needs a real path, so the first drag pulls the file to a temp copy — the row shows it copying, and a ⤓ appears when it's done. Drag it again and it drops. macOS only lets an app hand Finder a file during the drag it started, and a phone file isn't on this machine yet when that moment passes.
Folders don't drag — macOS wouldn't take a directory handed over that way. Use Copy to …, which is the better route for a folder anyway: it measures what's inside first, asks before it starts, and shows how much has actually moved.
Managing phone files (cable, USB debugging): New folder, Rename, Cut → Paste to move, and Delete.
Because a phone has no trash, deleting is guarded: a confirmation names exactly
what will go with Cancel as the default, names are validated so a rename can
never escape its folder, nothing silently overwrites, and every path is
shell-quoted before it reaches adb.
The most interesting bug in this project wasn't in my code.
Android phones expose file transfer over MTP, and the plan was to use
libmtp so no phone setup was needed. On macOS it fails, and the failure is
worth understanding:
Device 0 (VID=04e8 and PID=6860) is a Samsung Galaxy models (MTP).
error returned by libusb_claim_interface() = -3
LIBMTP PANIC: Unable to initialize device
Most phones present their MTP endpoint as USB interface class 6
(Still Image / PTP) — the same class as a camera. macOS automatically binds
its own Image Capture daemons (ptpcamerad, mscamerad-xpc) to any class-6
interface, and libusb cannot detach a kernel driver on macOS. So the port is
taken before any app gets a look in. Quitting Android File Transfer doesn't help,
and the Apple daemons are SIP-protected — they respawn instantly if you kill
them. This is why Android File Transfer was always unreliable on a Mac.
ioreg -p IOUSB -w0 -l tells the story: the phone's interface is right there,
labelled MTP@0 with bInterfaceClass = 6.
The fix was product, not code. Beam detects this exact failure and tells you
what's happening and what to do, instead of the empty file list that a naive
implementation shows. The adb backend then does the job properly for anyone
who turns on USB debugging.
A lesson worth keeping: an error you can't fix is still worth naming precisely. "No files found" is a bug report; "macOS is holding this phone — here's the alternative" is a product.
Discovery asks every address on your subnet whether it's running Beam. It's blunt, and it works on networks where mDNS quietly doesn't — mesh routers that don't forward multicast, guest Wi-Fi, corporate APs.
Blunt used to also mean slow, so the sweep runs in two passes. Addresses there's already a reason to care about go first — a device you've used before, plus everything in this machine's ARP table — with a generous timeout. The blind sweep of the rest follows with a short one. Devices appear as they answer rather than when the sweep finishes.
Two escape hatches for when that isn't enough:
- Devices you've used before are listed straight away, greyed out. Tapping one asks its last address directly instead of waiting for another sweep.
- Connect by IP — the
+button on the desktop, By IP on the phone. The address is on the other device's screen.
- Transfers are approved, but not yet encrypted. Nothing is written to disk until you accept it (see below), but the bytes themselves still cross your network in the clear, and a determined attacker on the same Wi-Fi could impersonate a device you have trusted. TLS is next.
- Cable features need USB debugging; MTP is read-only and blocked on macOS.
- iPhones can't use USB at all — Apple exposes no MTP or filesystem there.
- The Mac build is unsigned, and only Apple Silicon is built today.
- iOS keeps its receiver running only while the app is in the foreground. Android now runs a foreground service, so it keeps listening in the background; iOS gives no equivalent to a third-party app.
- Discovery is a subnet sweep, not mDNS. It works on networks where mDNS doesn't, but a network that blocks device-to-device traffic entirely will still find nothing — that's what Connect by IP is for.
Every transfer path was checked with matching MD5 checksums, not just a success message:
| Path | Result |
|---|---|
| Mac → Android (Wi-Fi) | ✅ checksum matched |
| Android → Mac (Wi-Fi) | ✅ checksum matched |
| Android → iPhone (Wi-Fi) | ✅ checksum matched |
| Mac → iPhone (Wi-Fi) | ✅ checksum matched |
| Mac ↔ Galaxy S23 (cable) | ✅ both directions, checksums matched |
| Phone file operations | ✅ on a physical Galaxy S23 |
| Approval: accept, decline, token reuse, trust | ✅ Mac, physical Galaxy S23, iPhone simulator |
| Folder drag → phone, and remembered devices | ✅ Mac → iPhone simulator, folder expanded to 5 files under one approval |
The Android foreground service and its notifications are built and installed in the release APK but were last exercised on a simulator-free build — they want a run on a physical phone before I'd call them verified.
- Receiver approval prompt + verification code — Mac, Android and iOS
- Remembered devices, fast reconnect, and connect by IP
- Transfer history, retry for failed files, per-file progress
- Folder drag-and-drop
- Background receiving on Android, with a notification when a file lands
- Search, sort, categories and previews in the desktop explorer
- TLS for transfers
- Copy/duplicate on the phone, and undo
- Code-signed and notarised Mac build, Windows and Linux builds
Built with Electron, React Native, and a deep dislike of uploading my own photos to somebody else's computer.
MIT licensed — see LICENSE.




