A macOS menu bar app that builds, signs, and serves your iOS apps over HTTPS. Install on any iPhone from anywhere with a single tap in Safari. Control everything from your phone with the iOS companion app, or from any browser with the built-in web app.
Deploying iOS builds to a test device is more painful than it should be. TestFlight takes 15-30 minutes to process each upload, USB deployment requires physical access to the device, and tools like Xcode's wireless debugging require both devices on the same network. If you're iterating quickly or your test device is somewhere else entirely, none of these options work well.
RemoteDeploy eliminates all of that. It builds your app, serves the signed IPA over HTTPS via Tailscale, and your iPhone installs it from a URL in Safari in about 10 seconds, from anywhere in the world. And you can kick off builds from your phone.
+-----------------------+ Tailscale VPN +--------------------+
| Mac (developer) |<------------------------------>| iPhone (remote) |
| | | |
| Menu Bar App: | HTTPS (port 8443) | Safari: |
| - Builds .ipa |<------------------------------>| - Opens install |
| - Serves via HTTPS | REST API + WebSocket | page |
| - REST API | | - Taps Install |
| - Bonjour discovery | HTTP (port 8080) | |
| |<------- local WiFi ----------->| Companion App: |
| Web PWA at /app/ | | - Trigger builds |
| | | - Live build log |
+-----------------------+ +--------------------+
The Mac runs xcodebuild to archive and export your app with ad-hoc signing, then serves the IPA over HTTPS (SwiftNIO). Tailscale provides the secure tunnel and valid TLS certificate. On the iPhone side, open a URL in Safari, tap Install, and the app appears on your home screen.
The Mac also exposes a REST API and WebSocket endpoint so you can trigger builds, watch logs, and manage projects from the iOS companion app or the built-in web PWA (works on any device with a browser).
- One-click build and deploy -- archive, sign, serve, and notify in a single action
- Works from anywhere -- Tailscale connects your Mac and iPhone across any network
- iOS companion app -- native SwiftUI app to trigger builds, watch live logs, and manage projects from your phone
- Web PWA -- pinnable web app at
/app/that works on any device (Android, iPad, desktop browser) - QR code pairing -- scan a QR code on your Mac to securely pair your phone in seconds
- Local WiFi support -- API works over plain HTTP on port 8080 when you're on the same network (no Tailscale needed for build control)
- Bonjour discovery -- companion app and web clients auto-discover your Mac on the local network
- Live build log streaming -- WebSocket-powered real-time xcodebuild output on phone or browser
- Multiple project support -- configure as many iOS projects as you want, each with its own install URL
- Push notifications -- get notified via Prowl, Pushover, or ntfy when builds finish
- macOS app builds -- build, serve, and auto-deploy macOS apps (not just iOS). Download
.app.zipfrom the install page or auto-deploy to the local machine - Local auto-deploy -- macOS builds can automatically quit the running app, replace it, and relaunch. RemoteDeploy can even deploy itself.
- REST API -- 20 endpoints for full programmatic control (
/api/v1/) - IPA import -- skip the build step entirely by importing a pre-built IPA
- Setup wizard -- a 5-step guided assistant handles Tailscale, certificates, and project configuration
- Protocol-oriented architecture -- every major component is defined as a protocol, making the codebase testable and extensible
Before you start, you need:
- macOS 14 or later
- Xcode -- full installation (not just Command Line Tools)
- Tailscale -- installed on both your Mac and iPhone, signed into the same tailnet
- Apple Developer Program membership -- with an ad-hoc provisioning profile that includes your test device's UDID
- XcodeGen -- if building from source (
brew install xcodegen)
git clone https://github.com/danrichardson/mac-remote-deploy.git
cd mac-remote-deploy
xcodegen generate
open RemoteDeploy.xcodeprojThis generates an Xcode project with three targets:
- RemoteDeploy -- the macOS menu bar app
- RemoteDeployCompanion -- the iOS companion app
- RemoteDeployTests / RemoteDeployIntegrationTests -- test suites
In Xcode, select each target and set your development team:
- RemoteDeploy target: select your team under Signing & Capabilities. This is a macOS app, so any Apple Developer account works.
- RemoteDeployCompanion target: select your team and set a unique bundle identifier (e.g.,
com.yourname.remotedeploy.companion). This is an iOS app and needs a valid provisioning profile to run on a device.
- Select the RemoteDeploy scheme, target My Mac
- Build and run (Cmd+R)
- The app appears as a package icon in your menu bar -- there is no dock icon or main window
- The Setup Assistant opens automatically on first launch
The setup wizard has 5 steps:
- Tailscale -- verifies Tailscale is installed and connected, shows your hostname
- Certificates -- generates TLS certificates via
tailscale cert(or lets you browse to existing ones) - Add Project -- pick your
.xcodeprojor.xcworkspace, auto-detects schemes, you fill in bundle ID and team ID - Push Notifications (optional) -- configure Prowl, Pushover, or ntfy with test buttons
- Done -- shows your install URL and a Copy button
- Click the menu bar icon
- Select your project from the dropdown
- Click Build & Deploy
- Watch the build log (click "View Build Log" for the full output)
- When the build succeeds, open the install URL in Safari on your iPhone
- Tap Install -- the app appears on your home screen in ~10 seconds
RemoteDeploy is meant to run continuously on your build machine. To install it
into /Applications and have it start automatically at login -- with no
runtime dependency on Xcode, DerivedData, or this source tree -- use the
one-command installer:
./deploy.shFirst time only:
chmod +x deploy.shif git didn't preserve the executable bit.
This will:
- Build a Release
.app(in/tmp-- it never touches a relocated DerivedData volume) - Stop the LaunchAgent so it can't relaunch the old binary mid-swap
- Gracefully quit the running RemoteDeploy and wait for port 8443 to free
- Install the fresh
.appinto/Applications - Install a LaunchAgent (
~/Library/LaunchAgents/com.remotedeploy.app.plist) that auto-starts the app at login and restarts it after a crash - Remove the legacy Login Item (so it can't double-launch alongside the agent)
- Start the new version via
launchd
The installed app in /Applications is fully self-contained: it does not read
from this repo or any ~/Library/Developer/Xcode folder at runtime, so moving or
deleting your DerivedData/dev directories will not affect it.
| Command | What it does |
|---|---|
./deploy.sh |
Fast install: signed (build-release.sh --skip-notarize), no Apple round-trip |
./deploy.sh --release |
Full signed + notarized build (distributable to other Macs) |
./deploy.sh --no-build |
Skip the build; just (re)install the last /tmp output |
The --release path requires notarization credentials configured for
build-release.sh (see that script's header). LaunchAgent stdout/stderr is
written to /tmp/remotedeploy.launchagent.log.
To uninstall the autostart behavior:
launchctl bootout gui/$(id -u)/com.remotedeploy.app
rm ~/Library/LaunchAgents/com.remotedeploy.app.plistThe companion app lets you trigger builds, watch live build logs, and manage your Mac from your phone.
- In Xcode, select the RemoteDeployCompanion scheme
- Connect your iPhone or select it as the destination
- Build and run (Cmd+R)
- On the Mac, open Settings (from the menu bar dropdown) and go to the Devices tab
- Click Pair New Device -- a QR code appears
- On your iPhone, open the companion app and tap Scan QR Code
- Point your camera at the QR code -- pairing completes automatically
The QR code contains your Mac's server URL and a one-time authentication token. The token is stored securely in the iOS Keychain and hashed (SHA-256) on the Mac -- the raw token is never written to disk on the Mac side.
- Projects tab -- see all configured projects, tap one for details, trigger a build
- Build tab -- select a project, tap Build & Deploy, watch the live build log stream via WebSocket
- Installs tab -- pull-to-refresh list of every IPA download (who, when, from where)
- Settings tab -- see server status, Tailscale connection, push notification config, disconnect
When your phone and Mac are on the same WiFi network, the companion app can discover your Mac automatically via Bonjour -- no Tailscale needed for build control. The Mac advertises itself as _remotedeploy._tcp on the local network.
Note: OTA app installation still requires Tailscale (iOS needs trusted HTTPS certificates for itms-services:// installs). But triggering builds, watching logs, and managing projects works fine over plain HTTP on port 8080.
The Mac serves a progressive web app at /app/ that works on any device with a browser.
Open your Mac's server URL with /app/ appended:
https://your-mac.tail12345.ts.net:8443/app/
Or on local WiFi (no Tailscale):
http://your-mac-ip:8080/app/
- iOS Safari: tap Share > Add to Home Screen
- Android Chrome: tap the three-dot menu > Add to Home Screen (or Install App)
- Desktop browsers: most Chromium browsers show an install prompt in the address bar
The first time you open the PWA, it asks for your bearer token. You can get this from the QR code pairing flow on your Mac (the token is shown as text below the QR code for manual entry). The token is stored in the browser's localStorage.
Everything the iOS companion app does:
- View and select projects
- Trigger builds
- Watch the live build log (WebSocket)
- View install history
- Check server and Tailscale status
- Disconnect
The Mac exposes a full REST API at /api/v1/ for programmatic control. All endpoints except POST /api/v1/pair require a bearer token in the Authorization header.
# All requests (except pairing) need this header:
-H "Authorization: Bearer YOUR_TOKEN_HERE"curl -H "Authorization: Bearer $TOKEN" \
https://your-mac.tail12345.ts.net:8443/api/v1/projectscurl -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{}' \
https://your-mac.tail12345.ts.net:8443/api/v1/projects/PROJECT_UUID/buildcurl -H "Authorization: Bearer $TOKEN" \
https://your-mac.tail12345.ts.net:8443/api/v1/status| Method | Path | Description |
|---|---|---|
| POST | /api/v1/pair |
Complete QR pairing (no auth required) |
| DELETE | /api/v1/pair |
Unpair the calling device |
| GET | /api/v1/status |
Server + Tailscale + build status |
| GET | /api/v1/projects |
List all projects |
| POST | /api/v1/projects |
Create a project |
| GET | /api/v1/projects/:id |
Get project detail |
| PUT | /api/v1/projects/:id |
Update a project |
| DELETE | /api/v1/projects/:id |
Delete a project |
| POST | /api/v1/projects/:id/build |
Trigger a build |
| GET | /api/v1/projects/:id/build |
Get build status |
| DELETE | /api/v1/projects/:id/build |
Cancel a build |
| GET | /api/v1/builds |
Build history |
| GET | /api/v1/installs |
Install history |
| GET | /api/v1/settings |
Get server settings |
| PUT | /api/v1/settings |
Update server settings |
| GET | /api/v1/filesystem/browse?path=... |
Browse Mac directories |
| GET | /api/v1/filesystem/schemes?path=... |
Detect Xcode schemes |
| GET | /api/v1/devices |
List paired devices |
| DELETE | /api/v1/devices/:id |
Revoke a paired device |
| GET | /api/v1/ws |
WebSocket (build logs, status) |
RemoteDeploy uses a protocol-oriented architecture. Every major component is defined as a Swift protocol with a concrete implementation.
| Protocol | Implementation | Purpose |
|---|---|---|
BuildEngineProtocol |
XcodeBuildEngine |
Wraps xcodebuild archive + export |
DeployServerProtocol |
NIODeployServer |
HTTPS + HTTP server via SwiftNIO |
TailscaleProviderProtocol |
CLITailscaleProvider |
Hostname detection, cert management |
ManifestGenerating |
ManifestGenerator |
Generates OTA manifest.plist |
InstallPageGenerating |
InstallPageGenerator |
Generates HTML install page |
ProjectStoring |
UserDefaultsProjectStore |
CRUD for project configurations |
PairedDeviceStoring |
JSONPairedDeviceStore |
CRUD for paired companion devices |
CertificateProviding |
TailscaleCertificateProvider |
Loads and refreshes TLS certs |
InstallTracking |
ServerInstallTracker |
Logs IPA downloads |
PushNotifying |
ProwlNotifier, PushoverNotifier, NtfyNotifier |
Push notifications on build events |
LocalDeployManagerProtocol |
LocalDeployManager |
Post-build local deploy (quit, copy, relaunch) |
mac-remote-deploy/
Packages/RemoteDeployShared/ # Shared SPM package (models, API types)
RemoteDeploy/ # macOS menu bar app
API/ # REST API router, auth, route handlers
Routes/ # Per-resource handlers
WebSocket/ # WebSocket manager + handler
Models/ # Re-exports from shared package
Protocols/ # Service protocols
Services/ # Concrete implementations
Views/ # SwiftUI views
Resources/pwa/ # Web PWA static files
RemoteDeployCompanion/ # iOS companion app
Services/ # APIClient, Keychain, Bonjour, WebSocket
Views/ # SwiftUI views (Discovery, Build, etc.)
RemoteDeployTests/ # Unit tests
RemoteDeployIntegrationTests/ # Integration tests (real NIO server)
docs/ # Documentation
project.yml # XcodeGen project definition
The RemoteDeployShared SPM package contains all model types and API DTOs. Both the Mac app and iOS companion depend on it, so data serializes identically on both sides. The package targets macOS 14+ and iOS 17+.
See docs/remote-deploy-server-spec.md for the full technical spec, docs/v2-changes.md for detailed v2 change notes, and CHANGELOG.md for the full release history.
| Port | Protocol | Purpose | When |
|---|---|---|---|
| 8443 | HTTPS (TLS) | OTA installs, API, PWA | Always (via Tailscale) |
| 8080 | HTTP (plain) | API, PWA (no OTA installs) | Local WiFi only |
OTA app installs require HTTPS with a trusted certificate (iOS requirement). The plain HTTP listener on 8080 is for API access and the web PWA when Tailscale isn't available.
Three push notification providers are supported. Enable any combination of them in Settings.
- Prowl -- iOS push notifications via the Prowl app. Requires an API key from prowlapp.com.
- Pushover -- cross-platform notifications with clickable install URLs. Requires an app token and user key from pushover.net.
- ntfy -- free, open-source notifications. Use the public server at ntfy.sh or self-host your own.
Each provider has a "Send Test Notification" button in Settings so you can verify the configuration works before relying on it.
The build runs on your Mac via xcodebuild. Make sure your project builds successfully in Xcode first. Check the live build log for the specific error.
- On Tailscale: make sure both devices are connected to the same tailnet and the Mac's server is running
- On local WiFi: make sure both devices are on the same network. The Mac advertises via Bonjour (
_remotedeploy._tcp). Check that your iPhone's local network permission is enabled for the companion app.
Make sure you granted camera access to the companion app. The QR code is valid for 10 minutes -- if it expires, generate a new one from Settings > Devices > Pair New Device.
This is expected. iOS requires HTTPS with a certificate from a trusted CA for OTA installs (itms-services:// protocol). Connect to Tailscale for OTA installs. Build triggering and monitoring work fine over plain HTTP.
Your bearer token is invalid or expired. Re-pair your device by scanning a new QR code, or re-enter the token in the web PWA.
# Generate the project first
xcodegen generate
# Unit tests
xcodebuild test -scheme RemoteDeployTests -destination 'platform=macOS'
# Integration tests (spins up a real NIO server)
xcodebuild test -scheme RemoteDeployIntegrationTests -destination 'platform=macOS'RemoteDeploy uses Apple's os.Logger (unified logging) for all production logs. Both targets are organized by subsystem and category so you can filter to exactly what you care about.
Subsystems:
com.remotedeploy.host— the macOS appcom.remotedeploy.companion— the iOS companion app
macOS host categories: server, api, pairing, build, tailscale, storage, notifications, bonjour, ui
iOS companion categories: pairing, api, ui
Streaming logs from the terminal:
# Everything from the macOS host
log stream --subsystem com.remotedeploy.host
# Just the API request log lines
log stream --subsystem com.remotedeploy.host --predicate 'category == "api"'
# Build engine errors only, with debug detail
log stream --subsystem com.remotedeploy.host --predicate 'category == "build"' --level debug
# iOS companion (when the device is connected)
log stream --subsystem com.remotedeploy.companion --predicate 'category == "pairing"'Console.app: open Console, click your Mac in the sidebar, and filter the search field with subsystem:com.remotedeploy.host or subsystem:com.remotedeploy.companion.
Privacy: by default os.Logger redacts user-identifying values (paths, hostnames, project names) in release builds — they show up as <private> in Console unless you've enabled the private-data debug profile. Status codes, HTTP methods, and durations are always public.
Contributions are welcome. Here's how to get started:
- Fork the repo and open a pull request against
main - Run tests before submitting (see above)
- All major components use protocols -- you can add new implementations (a new push notification provider, a different build engine, etc.) without changing existing code
- See docs/remote-deploy-server-spec.md for architecture details
MIT
RemoteDeploy is built by Throughline Tech, LLC.
Read the full deep dive on how RemoteDeploy was built: throughlinetech.net/deep-dives/remotedeploy