Architecture Overview
How TermOnMac Works
TermOnMac is a zero-knowledge relay between your iPhone and your Mac. The iOS app, the Mac daemon, and the Cloudflare Workers relay each do a specific job — and everything interesting happens at the boundaries. This page walks through the whole system in nine sections, each linking to a deeper engineering note grounded in the shipping source code.
1. The Three Parts
TermOnMac has three components: an iOS app, a Mac CLI daemon, and a relay server on Cloudflare Workers. All terminal data is end-to-end encrypted between the Mac and iPhone — the relay forwards ciphertext it cannot read.
2. Pairing & Identity
Pairing is a QR code scan. The QR carries a room_id and a one-time token. After pairing, the Mac's identity key is locked in via trust-on-first-use, so later reconnects are verified against the same key.
3. Cryptography
Every connection derives a fresh session key. Identity keys are persistent, per-connection ephemeral keys give forward secrecy, and an HMAC over the sorted ephemeral keys prevents a compromised relay from pulling off a key substitution.
4. The PTY
On the Mac side, each session is a real login shell spawned via forkpty(). The helper reads from the PTY without blocking a thread, buffers output in a way that doesn't flood the wire, and resizes correctly when the iPhone keyboard appears.
- forkpty() and the Login Shell argv[0] tricks, controlling TTY, and what $SHELL actually does.
- SHELL_SESSIONS_DISABLE and Friends Four environment variables that keep the shell sane.
- Non-Blocking PTY I/O with GCD Reading a PTY master FD without burning a thread.
- 32KB or 200ms, Whichever Comes First Why we buffer output — and intercept vim cursor queries locally.
- RingBuffer for Terminal Replay Restoring terminal state after a network drop.
- PTY Resize and Buffer Switches Propagating iPhone terminal-size changes back to the Mac.
5. The Relay
The relay is a Cloudflare Worker plus a Durable Object per room. DOs hibernate when idle, so we rebuild state from storage on every wake. Quota is credit-based with a 5-hour rolling window, and room limits are enforced by a KV prefix scan — no counters, no cleanup job.
- DO as a WebSocket State Machine Managing Mac and iPhone WebSocket slots per room.
- Surviving DO Hibernation Rebuilding state after an idle wake.
- Batching WebSocket Messages Why the relay coalesces up to 20 payloads per message.
- Room Limits Without a Database KV prefix scans, TTLs, and reconnect-always-allowed.
- Credit-Based Quota With a 5-Hour Window 1 credit per message, 2 credits per DO runtime minute.
- Subscription State in a Per-User DO Strong consistency for tier, expiry, and dedup.
- The KV R-M-W Race A real race that ate admin-issued quota — and the fix.
6. Reliability
Mobile networks drop, switch, and wake. TermOnMac reconnects on its own — exponential backoff with an immediate reconnect on a detected network change, a heartbeat cadence that keeps the relay DO awake during active use, and replay of missed output from the ring buffer.
7. The iOS Side
On the iPhone, the terminal is SwiftTerm wrapped in a UIViewRepresentable. A custom keyboard toolbar fills in the keys that iOS doesn't have, and scroll position is preserved even when new output arrives and when the PTY is reset on reconnect.
8. Auth, Billing, and Integration
The Mac CLI talks to its helper daemon over a Unix socket. The iOS app signs in via GitHub, Google, or Apple, and those identities are linked into a single user by email. App Store subscriptions are verified by hand-parsing Apple's JWS receipts, and subscription notifications are deduplicated across retries.
- Unix Domain Socket IPC How the short-lived CLI talks to the long-running daemon.
- Multi-Provider OAuth with Account Linking GitHub, Google, and Apple linked into a single user.
- API Key Lifecycle and Refresh 30-day sliding TTLs and rotating refresh tokens.
- Verifying Apple JWS Receipts by Hand Parsing the x5c chain with only Web Crypto.
- Reliable Apple Subscription Notifications Three layers against retrying, out-of-order deliveries.
9. Recurring Patterns
Across the relay, the Mac agent, and the iOS app, the same few ideas come up again and again: strong consistency inside a Durable Object, split KV keys to avoid read-modify-write races, version markers, lazy expiry, and channel binding wherever two parties share a secret.