A fast, fully peer-to-peer pair-programming app for macOS โ a Tuple clone with no accounts and no cloud. Add a peer by IP address, or by a shared code that rendezvouses through a tiny Cloudflare Worker; either way media flows directly between the two machines.
- Menu bar app โ lives in the status bar, no Dock icon.
- Direct P2P calls โ add a peer's IP, hit Call; they get a ring panel with Accept / Decline. Signaling runs over TCP (port 47800), media over UDP (port 47801). Nothing ever touches a third-party server.
- Shared-code calls (no Tailscale/VPN needed) โ both people add the same
code word instead of an IP. A Cloudflare Worker + Durable Object
(
server/) acts as a rendezvous room: it carries presence and the tiny JSON call-signaling messages, while audio/video are hole-punched directly peer-to-peer over UDP (STUN via Cloudflare/Google for the public endpoint; no media relay, so it fails rather than falls back if both NATs block punching โ LAN and Tailscale paths are tried too). - Floating video window โ a small always-on-top square showing the peer's camera. Hover over the video and it turns translucent and click-through, so it never blocks your work; drag it by its top bar. Toggle camera and mic from the menu bar.
- Low-latency screen sharing โ ScreenCaptureKit capture โ hardware H.264 (VideoToolbox low-latency rate control, up to 60 fps, native resolution) โ UDP โ hardware decode. No retransmission delays: lost frames trigger an instant keyframe request instead of stalling the stream.
- Annotations โ the viewer click-drags on your shared screen to draw; strokes appear live on the sharer's actual screen (excluded from capture so they don't echo) and fade after a few seconds.
- Voice โ Opus at 48 kHz with Apple's voice processing (echo cancellation
- noise suppression), 20 ms frames.
./build.sh
open build/Pairwise.appRequires macOS 14+. On first use macOS will prompt for microphone, camera, and (when you share) screen-recording permission.
build.sh signs with your "Developer ID Application" certificate if one is in
the keychain (hardened runtime, notarization-ready); otherwise it falls back
to ad-hoc signing for local development.
Updates ship via Sparkle: the app checks
appcast.xml on the main branch and downloads the zip from the matching
GitHub release.
Releases are automatic: every push to main runs the GitHub Actions
Release workflow (.github/workflows/release.yml), which picks the next
1.N version from the newest tag and runs release.sh on the runner โ
build, sign, notarize, staple, zip, EdDSA-sign the update, add an appcast
entry, create the GitHub release, and push. Don't run release.sh locally.
Release notes (on both the GitHub release and Sparkle's update dialog) are the commit subjects since the previous release tag โ usually the single commit that triggered the release.
The workflow needs these repository secrets (documented at the top of
release.yml): DEVELOPER_ID_CERT_P12, DEVELOPER_ID_CERT_PASSWORD,
APPLE_ID, APPLE_TEAM_ID, APPLE_APP_PASSWORD, SPARKLE_PRIVATE_KEY.
With a shared code (easiest):
- Agree on a code word with your pair (e.g.
ben-jerome). - Both: Add Peerโฆ โ enter the other person's name and the code word.
- The presence dot turns green when they're online; Call
<name>rings them, and media connects directly via UDP hole punching.
With an IP address:
- Both people run Pairwise on the same network (or with UDP/TCP ports 47800
- 47801 reachable, e.g. via Tailscale or port forwarding).
- Find your IP (for Tailscale:
tailscale ip -4) and tell it to your pair. - Add Peerโฆ โ enter their name and IP.
Then: Call <name> โ they accept, and audio + your camera start
immediately. Share My Screen from the menu; their viewer window opens
automatically and they can draw on it any time. Only one person shares at a
time โ starting a share while your pair is sharing takes it over.
Currently IPv4 only.
A Cloudflare Worker with one Durable Object per room code, using the
WebSocket Hibernation API (idle rooms cost nothing). It only ever sees
presence events and small JSON signaling messages โ never media. Deployed at
https://pairwise-rendezvous.benborgers.workers.dev; the app can be pointed
elsewhere via
defaults write dev.benborgers.pairwise pairwise.rendezvousURL wss://โฆ.
Pushes to main that touch server/ deploy it automatically
(.github/workflows/deploy-server.yml, needs the CLOUDFLARE_API_TOKEN
repository secret); those pushes don't cut an app release. Manual deploy:
cd server && npx wrangler deploy| Piece | Technology |
|---|---|
| Signaling / control | Network.framework TCP, length-prefixed JSON |
| Media transport | Raw BSD UDP socket, custom fragmentation (1200-byte MTU), keyframe-request loss recovery |
| Video encode | VideoToolbox H.264, low-latency rate control, real-time mode |
| Video decode + display | AVSampleBufferDisplayLayer (hardware decode, display-immediately) |
| Screen capture | ScreenCaptureKit, native pixel scale, 60 fps |
| Audio | AVAudioEngine + voice processing, Opus via AVAudioConverter (PCM fallback) |
| UI | AppKit (status item, borderless floating windows, overlay windows) |