Engineering Notes
Technical notes on building TermOnMac — a zero-knowledge iPhone ↔ Mac terminal relay. Every article is grounded in the actual source code of the shipping app.
Architecture
How the pieces fit together — iOS app, Mac daemon, relay.
How TermOnMac Works: A Zero-Knowledge Mac Terminal Relay
How an iPhone controls a Mac terminal over the internet — iOS app, Mac CLI daemon, Cloudflare Workers relay. End-to-end encrypted; the relay forwards ciphertext it cannot read.
Read article →The attach/detach Model: PTY Keeps Running While You're Away
How TermOnMac separates PTY session lifetime from iOS attachment — two session lists, buffer-only mode, draft input preservation, and full-remove destruction via ptyDestroy.
Read article →Session Takeover: When a Second Device Connects to the Same PTY
How TermOnMac handles two iOS devices racing to attach to the same PTY — an isTakenOver flag, explicit takeover callbacks, and relay-enforced single-iOS-socket per room.
Read article →Cryptography
Key exchange, session keys, identity, and forward secrecy.
X25519 Key Exchange Without a Central Key Server: How QR Pairing Works
QR pairing, persistent identity keys, and per-connection ephemeral keys. How two devices derive a shared AES-256-GCM session key without a central key server.
Read article →Why We Don't Use Raw ECDH Output: HKDF Session Key Derivation
Why we don't use the raw X25519 shared secret as an AES key. HKDF-SHA256 with a versioned, nonce-salted construction gives per-connection key separation and protocol versioning.
Read article →Channel Binding with HMAC: Preventing Relay-Level MITM Attacks
An HMAC over both sorted ephemeral public keys — keyed by the room secret — prevents a compromised relay from substituting keys during the handshake.
Read article →Forward Secrecy in a Reconnectable Protocol: Two Key Pairs Per Session
Why TermOnMac uses a persistent identity key for TOFU and a per-connection ephemeral X25519 key for ECDH — so a stolen identity key cannot decrypt past recorded traffic.
Read article →Trust On First Use: How TermOnMac Verifies Your Mac's Identity on Reconnect
The first identity key registered for a room is trusted. How TermOnMac rotates the room secret and replaces sockets without breaking the active session.
Read article →Relay & Durable Objects
WebSocket state machines, hibernation, quota, and batching.
Using Durable Objects as a WebSocket Session State Machine
How TermOnMac's Cloudflare Durable Object manages Mac and iPhone WebSocket slots, handles quota checks at connection time, and survives hibernation via tagged socket recovery.
Read article →Surviving Cloudflare DO Hibernation Without Dropping WebSocket Connections
Cloudflare Durable Objects hibernate when idle, losing in-memory state. How TermOnMac rebuilds WebSocket role mappings from storage-backed UUID tags on every wake.
Read article →Per-User Strong Consistency: Subscription State in a Durable Object
Why TermOnMac's subscription tier lives in a per-user Durable Object instead of KV — serialized writes, lazy expiry, notification dedup, and stale downgrade protection.
Read article →Building a Credit-Based Quota System with 5-Hour Rolling Windows
TermOnMac charges 1 credit per relay message and 2 per minute of DO runtime. How the 5-hour window, welcome bonus, and split KV keys protect admin grants from stale writes.
Read article →Enforcing Per-User Room Limits Without a Database
TermOnMac caps concurrent Mac rooms per tier using a Cloudflare KV prefix scan with 15-minute TTLs — no counters, no cleanup job, reconnects always allowed.
Read article →Batching WebSocket Messages: How relay_batch Reduces Overhead
Why TermOnMac coalesces up to 20 encrypted payloads per WebSocket message — not for framing overhead, but because the relay charges 1 credit per batch instead of per payload.
Read article →The R-M-W Race in Cloudflare KV: A Quota Bug and How We Fixed It
A concrete Cloudflare KV read-modify-write race that let stale Room DO flushes silently consume admin-issued quota grants — and the split-key plus version-marker fix.
Read article →PTY & Shell
forkpty, non-blocking I/O, resize, and terminal replay.
forkpty() on macOS: The Login Shell Setup
How TermOnMac spawns a login shell via forkpty() on macOS — argv[0] tricks, controlling TTY setup, and why SHELL_SESSIONS_DISABLE matters for a programmatic PTY.
Read article →SHELL_SESSIONS_DISABLE and Other macOS Shell Environment Setup
The four environment variables TermOnMac sets before execvp — TERM, LANG, SHELL_SESSIONS_DISABLE, TERMONMAC_SESSION — and why each matters for a programmatic PTY.
Read article →Non-Blocking PTY I/O with GCD Dispatch Sources
How TermOnMac reads from the PTY master FD without blocking a thread — O_NONBLOCK plus a DispatchSourceRead on the userInteractive queue, with EAGAIN-aware writes.
Read article →PTY Output Buffering: 32KB or 200ms, Whichever Comes First
Why TermOnMac doesn't send PTY output on every read — a dual-trigger buffer plus a local terminal query interceptor that answers vim's cursor queries without a round trip.
Read article →RingBuffer for Terminal Replay: Restoring State After Reconnect
Each TermOnMac PTY session keeps a per-session RingBuffer so the iPhone can replay the current terminal state after a network drop, with incremental offset-based delta delivery.
Read article →PTY Resize: Buffer Switch and Reinit Triggers in SwiftTerm
How TermOnMac propagates iPhone terminal size changes to the Mac via TIOCSWINSZ — buffer switches, reinit callbacks, Ctrl-L prompt redraws, and debounced resize events.
Read article →iOS Terminal UX
SwiftTerm integration, keyboard, and scroll behaviour.
Integrating SwiftTerm into a SwiftUI App
How TermOnMac wraps SwiftTerm's CustomTerminalView in a UIViewRepresentable for SwiftUI — feeding PTY bytes, handling resets on reconnect, and blocking accidental pastes.
Read article →Building a Terminal Keyboard Toolbar on iOS
iOS keyboards don't have Ctrl, Esc, or arrow keys. How TermOnMac builds a custom inputAccessoryView with debounced shortcut search, modifier toggles, and focus handling.
Read article →Preserving Scroll Position While Terminal Output Keeps Arriving
How TermOnMac keeps your reading position when you're scrolled up and new output arrives — saved scroll ratios, reset-before-replay, and local scroll shortcuts.
Read article →Networking & Reliability
Reconnect strategy, heartbeats, and network transitions.
Reconnecting Without Hammering the Server: Exponential Backoff with NetworkMonitor
How TermOnMac's iOS app handles lost WebSocket connections — exponential backoff, NWPathMonitor-driven immediate reconnect, input buffering, and a 5-minute total timeout.
Read article →Heartbeat Design: Keeping Durable Objects Awake
Why TermOnMac's Mac heartbeat flips between 30s and 1s during active use — Cloudflare DO hibernation, not dead-connection detection, is the real driver behind the cadence.
Read article →System Integration
OAuth, pairing tokens, API keys, and StoreKit receipts.
Unix Domain Socket IPC: How the Mac CLI Talks to the Helper Daemon
How TermOnMac's short-lived CLI talks to the long-running helper daemon — a chmod 0600 Unix socket with a length-prefixed JSON protocol and explicit IPC versioning.
Read article →Multi-Provider OAuth with Account Linking: GitHub, Google, and Apple Sign In
How TermOnMac links GitHub, Google, and Apple logins to a single user by email — KV key layout, account linking, pending-delete reversal, and email index backfill.
Read article →API Key Lifecycle: 30-Day TTL, Refresh Tokens, and Sliding Expiry
TermOnMac's auth model uses 128-bit API keys with 30-day sliding TTLs and 512-bit refresh tokens that rotate on use, with a 5-minute grace period for client crash recovery.
Read article →QR Code as a Secure Pairing Mechanism: One-Time Token and TOFU
How TermOnMac uses a single-use QR pairing token plus a rotated room secret and TOFU identity key — so even a photographed QR code becomes useless after the legitimate pair completes.
Read article →Verifying Apple JWS Receipts Without a Third-Party Library
StoreKit 2 JWS receipts carry an x5c cert chain, not a JWK. How TermOnMac hand-rolls a DER parser to extract SPKI and verify ECDSA P-256 signatures using only Web Crypto.
Read article →Reliable Apple Subscription Notifications with Durable Objects
Three layers of protection against Apple's retrying, out-of-order App Store Server Notifications — UUID dedup, stale-downgrade rejection, and per-user DO serialization.
Read article →Lessons Learned
Post-mortems and reflections from shipping the app.