← Back to home

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.

Architecture · · 4 min read

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 →
Architecture · · 3 min read

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 →
Architecture · · 2 min read

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.

Cryptography · · 3 min read

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 →
Cryptography · · 3 min read

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 →
Cryptography · · 3 min read

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 →
Cryptography · · 3 min read

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 →
Cryptography · · 3 min read

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.

Relay & Durable Objects · · 4 min read

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 →
Relay & Durable Objects · · 3 min read

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 →
Relay & Durable Objects · · 4 min read

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 →
Relay & Durable Objects · · 3 min read

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 →
Relay & Durable Objects · · 3 min read

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 →
Relay & Durable Objects · · 3 min read

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 →
Relay & Durable Objects · · 5 min read

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.

PTY & Shell · · 4 min read

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 →
PTY & Shell · · 3 min read

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 →
PTY & Shell · · 3 min read

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 & Shell · · 3 min read

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 →
PTY & Shell · · 3 min read

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 & Shell · · 3 min read

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.

iOS Terminal UX · · 3 min read

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 →
iOS Terminal UX · · 3 min read

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 →
iOS Terminal UX · · 3 min read

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.

Networking & Reliability · · 3 min read

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 →
Networking & Reliability · · 5 min read

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.

System Integration · · 4 min read

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 →
System Integration · · 3 min read

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 →
System Integration · · 4 min read

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 →
System Integration · · 4 min read

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 →
System Integration · · 4 min read

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 →
System Integration · · 4 min read

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.

Lessons Learned · · 6 min read

Recurring Architectural Patterns in TermOnMac

Ten recurring patterns across the TermOnMac relay, Mac agent, and iOS app — strong consistency in DO, split KV keys, version markers, lazy expiry, and channel binding.

Read article →