Thirty minutes, once.
Remotly is personal infrastructure: your herdr host runs the bridge, your phones pair with it, and only devices on your Tailscale tailnet can reach it. Nobody joins an existing bridge; everyone runs their own.
Requirements
| Where | What |
|---|---|
| Host | Linux or macOS running herdr 0.8 or newer as your user (socket ~/.config/herdr/herdr.sock); Node 24 (fnm install 24); openssl, stty; systemd --user for the service on Linux. |
| Network | Tailscale on the host and on the phones, same tailnet. Recommended everywhere, required for iPhones. Without it the bridge listens on all interfaces with a self-signed certificate that the Android app pins. |
| Phones | iPhone or iPad on iOS 17 or newer; Android 8 (API 26) or newer. Lock-screen approval with the unlock check needs Android 12. |
| Beta invite | Access to the TestFlight group and the Play internal-testing track, the repository, and the push credentials below. Ask for one. |
1. Get the apps
Accept the TestFlight invite on iPhone or iPad and install Remotly. On Android, open the internal-testing opt-in link once, then install Remotly from Google Play; it updates like any other app from then on. Builds land in both places automatically.
2. Prepare the host
herdr must already be running as you. Optional but worth it: resume_agents_on_restore in herdr’s config, so a reboot brings your agents back too. Then let Tailscale hand out certificates:
$ sudo tailscale set --operator=$USER # lets the bridge request certificates # then in the Tailscale admin console → DNS: enable MagicDNS and HTTPS Certificates
iPhones refuse the self-signed fallback certificate, so MagicDNS and HTTPS Certificates are not optional for iOS.
3. Install the bridge
# on the host that runs herdr $ git clone https://github.com/inferenceaftermath/remotly ~/remotly $ cd ~/remotly/bridge && npm ci --omit=dev $ node src/main.ts serve # first run writes ~/.config/remotly/config.json and the TLS material # as a service that survives logout $ scripts/install-service.sh # renders and starts the systemd --user unit remotly-bridge.service $ loginctl enable-linger $USER $ remotly-bridge doctor # Node, herdr socket, Tailscale, certificate, secrets, unit, reachability
The bridge reads herdr’s Unix socket, keeps one structural subscription open, and polls only the pane a phone is watching. Its configuration lives in ~/.config/remotly/config.json; the defaults are right for a Tailscale host:
{
"listen": { "host": "auto", "port": 7460 },
"tls": { "mode": "auto" }, // auto → Tailscale certificate, else self-signed
"security": { "require_tailnet": "auto" }, // auto → true when Tailscale is present
"push": { "include_excerpt": true, "debounce_ms": 2500 },
"approvals":{ "strict_verify": true }
} 4. Push credentials
The bridge sends notifications itself, so it needs the app’s push keys. Your beta invite includes them. Store them here:
~/.config/remotly/secrets/AuthKey.p8 # APNs auth key (iOS notifications) ~/.config/remotly/secrets/fcm-service-account.json # Firebase service account (Android notifications) # both mode 0600; without them everything works except notifications
5. Pair your phones
- Install Tailscale on the phone, log into the same tailnet and turn it on. Let it start by itself so that a notification action works while the app is closed: iOS → Tailscale → Settings → VPN On Demand; Android → system Settings → Network → VPN → Tailscale → Always-on VPN. Phones allow one VPN at a time.
- On the host, print a code and scan it from Remotly → Pair:
$ remotly-bridge pair --ttl 600 # prints a QR and the manual fields; one code per phone $ remotly-bridge devices list # who is paired; revoke <id> to throw one out $ remotly-bridge push-test <device_id> # a banner should appear on that phone
- One code per device. Codes are single-use and expire; five wrong codes lock that phone out for fifteen minutes.
- Allow notifications when the app asks, including Time Sensitive on iOS.
push-testshould then show a banner.
6. Daily use
- Open the app: the tabs and panes of your herdr session appear live. Tap one to watch it; the bridge fits the program to your phone’s width and releases the fit when you leave.
- Type in the composer, use the key row (Esc, Tab, Ctrl, arrows, Enter). The + button attaches photos or opens a new terminal on the desktop, ready to run
claude,codexorpi. - Swipe up to read scrollback. Programs that draw their own screen (Claude Code, vim, less) get swipes as mouse-wheel steps instead; the pane menu can pin either behaviour.
- Long-press a word to select, drag the handles, Copy. The pane menu also has Copy screen, Tell me when it’s done, and Close terminal.
- A− / A+ in the title bar step the terminal text size; Settings has Require unlock to approve, Show working agents, and Tell me when it’s done by default.
Bridge commands
| Command | Purpose |
|---|---|
serve | Run the daemon (what the systemd unit runs). |
pair [--manual] [--ttl 300] | Create a pairing code and print the QR plus the manual fields. |
devices list / devices revoke <id> | Paired devices; revoke one. |
status | herdr connectivity, listener, certificate mode and expiry, devices, push readiness, connected clients. |
push-test <device_id> | Send a synthetic approval notification to one device. |
doctor | Node, herdr socket and protocol, Tailscale, certificate, secrets permissions, unit, daemon reachability. |
When something is off
- Pairing fails. Same tailnet on both ends?
statusshould showtls: tailscale. Five wrong codes lock that phone out for fifteen minutes; restarting the bridge clears the counters. - iPhone will not connect. It is almost always the certificate. Enable HTTPS Certificates in the Tailscale admin console, run
sudo tailscale set --operator=$USER, restart the bridge. - No notifications. Check the app was allowed to notify (it asks right after pairing), that
statusreportsapns=ready/fcm=ready, and that the phone is not currently viewing the pane (push for the pane you are looking at is suppressed on purpose). - The desktop pane is narrow after a restart. Any herdr layout change restores it; the bridge restores fits itself on a clean shutdown.