Documentation
Pair the iOS companion
Connect a native iOS build to a running desktop, verify the certificate, and control device access.
Check the two ends of the connection
The companion is a client of a running desktop sidecar. It does not host the project database, geometry kernel, model provider, or workflow runner. Before pairing, confirm that the desktop opens its projects and that the phone can route to the desktop through Wi-Fi, Personal Hotspot, or an already configured overlay/VPN network.
Use a native iOS build containing modules/companion-net. Expo Go lacks this module and cannot make the pinned connection. The package also has a browser development fallback, but browser fetch does not implement the native pinning policy. Android configuration in the manifest does not supply an Android equivalent of the Swift module.
bun install
bun run --cwd apps/mobile typecheck
bun run --cwd apps/mobile test
bun run --cwd apps/mobile ios The iOS run command requires macOS, Xcode, and CocoaPods. A physical-device build requires a trusted device and signing team. bun run --cwd apps/mobile build exports JavaScript; it does not create an installable IPA. See the mobile reference for layout and transport implementation.
Enable the desktop gateway
Open desktop Settings → Devices and its Mobile companion panel, or use /settings/companion. Turn the gateway on, then create a named pairing and show its QR code. This is a separate listener from the ordinary loopback API: HTTPS on IPv4 0.0.0.0, port 8443 by default.
The service accepts integer ports 1024–65535. If another application occupies the selected port, enabling fails; choose a free port and create a link with the resulting address. Allow the connection through the desktop firewall for the network you intend to use. The application does not create a public relay, port forwarding, or a VPN.
Enabled state and port are persisted. After a sidecar restart, the gateway attempts to resume once the local API is ready. Certificate and private key files live under the sidecar data directory's companion/ subdirectory; the private key is written with owner-only permissions. The native app trusts the certificate from pairing, not a generic promise that every local certificate is safe.
Inspect and enable through the desktop API
For a reproducible development setup, use the launch token of an isolated sidecar you control. Replace port 43111 with its ready-event port. These are desktop-management calls; the phone token is not allowed to administer its own gateway.
CADCAM_URL=http://127.0.0.1:43111
CADCAM_TOKEN=local-development-token
CADCAM_PAIR_DIR=$(mktemp -d /tmp/cadcamfun-pair-guide.XXXXXX)
curl --fail-with-body "$CADCAM_URL/api/companion" -H "Authorization: Bearer $CADCAM_TOKEN"
curl --fail-with-body -X POST "$CADCAM_URL/api/companion/enable" -H "Authorization: Bearer $CADCAM_TOKEN" -H 'Content-Type: application/json' -d '{"port":18443}' Status contains enabled state, nullable port/fingerprint, suggested host, and device entries. The fingerprint can be null before a certificate exists. A public health probe establishes loopback process health; it does not prove that the HTTPS gateway is enabled, reachable, or accepting a device token. Use the Companion API for exact management schemas.
Create one named pairing
curl --fail-with-body -X POST "$CADCAM_URL/api/companion/pair" -H "Authorization: Bearer $CADCAM_TOKEN" -H 'Content-Type: application/json' -d '{"name":"Workshop iPhone","host":"192.168.1.20"}' -o "$CADCAM_PAIR_DIR/pairing.json"
CADCAM_DEVICE_ID=$(bun --no-env-file -e 'console.write((await Bun.file(process.argv[1]).json()).id)' "$CADCAM_PAIR_DIR/pairing.json") Replace the example host with an address the phone can reach, or omit the optional host to use discovery. The name is trimmed and must contain 1–60 characters. Success is HTTP 201 with id and link. Creating a token does not enable the listener; enable it separately.
The link carries the device token, certificate fingerprint, primary HTTPS origin, name, and optional alternate hosts. Display or transfer it privately to the phone; it is a credential. The desktop stores the token hash rather than its plaintext. The record has no expiry field in this implementation, so revoke an obsolete device explicitly rather than assuming it will expire.
Load the pairing on iOS
- Open the installed CADCAMFUN app's Connect screen.
- Choose Scan the pairing QR code and grant camera access, or use Paste a pairing link.
- For manual entry, provide the desktop address, token, and complete certificate fingerprint through the connection form.
- Allow local-network access when iOS prompts. Camera access and network access are separate permissions.
- Wait for the health/bootstrap probe to succeed and the app to open Projects.
The parser requires an HTTPS origin without user information, path content, query, or fragment. Tokens must be 16–4096 characters without whitespace/control characters; fingerprints are exactly 64 hexadecimal characters. Invalid links are refused before requests start.
Discovered alternate origins are deduplicated. The client races candidates, uses the first successful connection, aborts the others, and saves the successful origin. Discovery advertises up to six suitable private IPv4 addresses, including Wi-Fi, hotspot, and Tailscale-style IPv4 ranges; the current IPv4 listener does not advertise discovered IPv6 hosts.
What certificate pinning checks
The Swift transport uses ephemeral URLSession instances and a custom trust delegate. It obtains the leaf certificate's DER bytes, calculates SHA-256, and accepts the connection only when that hash equals the paired fingerprint. HTTPS is required and redirects are refused. Native callers do not fall back silently to unpinned fetch if the module is missing.
This supports the desktop's self-signed certificate without accepting arbitrary self-signed servers. The trust decision comes from the pairing QR/link. Replacing the desktop certificate changes that decision; create a fresh trusted pairing instead of suppressing the mismatch.
Ordinary browser/curl trust errors are not equivalent to testing the app's native delegate. A browser may reject a correctly pinned self-signed server because it uses a different trust store. Verify through the installed app and inspect the actual failure rather than disabling certificate validation globally.
After successful device authentication, the gateway records a last-seen timestamp for its current service state. Refresh desktop device status to confirm authenticated traffic. This indicates token use; it is not proof that every project action succeeded or a durable security audit trail.
Understand the device token scope
| Allowed through the gateway | Kept on the desktop API |
|---|---|
| Project list/detail; project events | Direct filesystem, file uploads, and credential management |
| VCS history/status and currently commit/branch/checkout/restore | GitHub remote configuration, push, and pull endpoints |
| Pull-request review/merge/close and workflow operations | Plugin and billing administration |
| Notifications and assistant status/models/agents | Companion enable/pair/revoke administration |
| Streaming chat, queue/abort, permission replies, session state/control | Other methods/paths absent from the allow-list |
The device token is limited by the method/path allow-list. It is not inherently read-only and does not select one project. Accepted requests are forwarded under the desktop's application identity and remain subject to endpoint authorization. Never describe pairing as merely granting passive monitoring.
Assistant permissions and pending questions are separate session interactions. Answer them in the mobile UI when requested. A token that can reach the reply endpoint does not mean every tool action has already been approved.
Use sessions and recover connectivity
Choose a project to inspect its overview, history, pulls, and workflow runs. Assistant sessions execute on the desktop with its provider settings. Configure the provider there first. The phone remembers a selected project and last session per project for the paired desktop; changing desktop identity clears that mapping.
Foreground checks probe with a 15-second budget. Failed checks back off up to 30 seconds, and bringing the app forward triggers another probe. Background state is tracked separately from offline state. Tap the desktop row in phone Settings to recheck manually.
Queued assistant messages are persisted with a maximum of 50 entries, dropping the oldest beyond that limit. They retry later and may retain their session identifier. This is bounded message delivery support, not offline CAD computation or exactly-once execution. After an interrupted write/turn, inspect the desktop session and project before repeating the same change.
Revoke or unpair deliberately
curl --fail-with-body -X DELETE "$CADCAM_URL/api/companion/devices/$CADCAM_DEVICE_ID" -H "Authorization: Bearer $CADCAM_TOKEN"
curl --fail-with-body "$CADCAM_URL/api/companion" -H "Authorization: Bearer $CADCAM_TOKEN" Tokens are checked on every new request, so subsequent requests using the revoked token fail. Revocation does not explicitly terminate an already forwarded stream. Disabling the gateway stops the listener; POST /api/companion/disable is the administrative endpoint.
Phone Settings → Unpair this phone deletes the local saved pairing and returns to Connect. It does not call the desktop revoke endpoint. Use both actions when retiring a device. To rotate only the device credential, revoke it and issue a new pairing; replacing the whole certificate also changes every phone's pin.
Diagnose failures by layer
| Result | Next check |
|---|---|
| No native pinning module | Open a custom native iOS build instead of Expo Go or an unsupported native platform. |
| Desktop did not answer | Gateway enabled, host/port correct, desktop awake, firewall, local-network permission, and Wi-Fi/VPN route. |
| Certificate mismatch | Verify the current desktop identity and create a new trusted pairing after certificate replacement. |
| 401 | Missing, incorrect, or revoked device token. |
| 403 | Method/path outside the route allow-list; an endpoint's own authorization can also refuse work. |
| 429 | Repeated failed authentication from the same address; the gateway tracks ten failures within a 60-second window. |
| 502 | The HTTPS listener could not reach its local upstream API. |
Native CI builds an unsigned simulator app and checks real gateway pairing/project listing. Separate CI checks typecheck, logic tests, and JavaScript export. These do not prove physical-device signing, TestFlight distribution, remote-network routing, or a real model provider.
Repeated bad tokens are throttled: ten failed attempts from one IP address within 60 seconds make further requests from it return HTTP 429 until the window expires. A 429 after re-pairing usually means an old device token is still being retried.
Implementation map
The management contract is packages/api/src/groups/companion.ts. Sidecar lifecycle and route policy are in services/companion.ts and companion-gateway.ts. Phone parsing, connection recovery, storage, and UI are in lib/pairing.ts, server.tsx, storage.ts, and app/connect.tsx. The native trust delegate is modules/companion-net/ios/CompanionNetModule.swift.
For a support report, retain the app version, connection method, non-secret gateway host/port, exact error/status, and whether the native module is present. Redact tokens, pairing links, model credentials, and private keys. Add the System API version result when comparing two development processes.
What a paired phone can call
Every request through the gateway must carry a device token and match this allow-list. Everything else, including credentials, plugins, files, billing, and /api/companion itself, is refused. Admitted requests are forwarded to the loopback API with the application token, which the phone never sees.
| Methods | Path pattern | Purpose |
|---|---|---|
| GET | /api/health | Reachability check. |
| GET | /api/projects, /api/projects/:id | Project list and header. |
| GET, POST | /api/projects/:id/vcs/* (except remote, push, pull), /pulls/*, /actions/* | History, pull requests, reviews, and workflow runs. GitHub sync stays on the desktop because it needs the owner's GitHub token. |
| GET | /api/projects/:id/events | Live SSE stream of history, workflow, and assistant activity; the phone's only push channel. |
| GET, POST, PUT, DELETE | /api/notifications, /mark-all-read, /:id | Read, mark read, and delete notifications. |
| GET | /api/ai/status, /models, /agents | Assistant discovery. |
| POST | /api/ai/chat/stream, chat/abort, chat/queue, permission/reply, sessions/state, sessions/control | Assistant sessions. |
Current scope of history writes
The history pattern currently also matches commit, checkout, and restore. Treat a paired phone as able to change project history, and revoke devices you no longer control.
Pairing link anatomy
POST /api/companion/pair with a device name (and optionally a host) creates a device token and returns its ID together with a link, which the desktop shows as a QR code:
cadcamfun://pair?host=https%3A%2F%2F192.168.1.20%3A8443&token=<43-char base64url>&fp=<sha256 hex>&name=Ada%27s+iPhone&hosts=https%3A%2F%2F100.64.0.7%3A8443 | Parameter | Content |
|---|---|
host | Preferred origin: the best private address of the desktop and the gateway port. |
token | 32 random bytes, base64url-encoded. It is shown once; the desktop stores only its SHA-256 hash, and device IDs are 12 hex characters. |
fp | SHA-256 fingerprint of the gateway certificate, lowercase hex without colons. The phone pins it. |
name | Device name shown in the devices list. |
hosts | Up to five further candidate origins (six addresses in total), which the phone races to find one that answers. |
The certificate is a self-signed P-256 certificate whose subject alternative names include localhost, 127.0.0.1, and the machine's reachable private addresses. It is kept in <dataDir>/companion, so pairings survive restarts. Replacing it changes the fingerprint and requires pairing again.
Gateway administration API
| Route | Body | Result |
|---|---|---|
GET /api/companion | — | enabled, port, fingerprint, host, and devices (id, name, createdAt, lastSeen). |
POST /api/companion/enable | optional port (default 8443, or 1024–65535) | Starts the HTTPS listener on 0.0.0.0 and persists the setting so it resumes at next launch. |
POST /api/companion/disable | — | Stops the listener. |
POST /api/companion/pair | name, optional host | { id, link }. |
DELETE /api/companion/devices/:id | — | Revokes one device token immediately. |
Failed bearer attempts are throttled per IP address: after 10 failures within 60 seconds, the gateway answers HTTP 429 ("Too many failed attempts") until the window passes. A wrong or revoked token otherwise gets HTTP 401.
Gateway status, captured
{ "enabled": false, "port": null, "fingerprint": null, "host": "172.30.0.2", "devices": [] } | Field | Meaning |
|---|---|
enabled | Whether the HTTPS gateway is running; off by default |
port | Gateway port while enabled, otherwise null |
fingerprint | SHA-256 fingerprint of the gateway certificate the phone pins; null until a certificate exists |
host | The LAN address the pairing link will advertise |
devices | Paired devices; each can be revoked on its own |
If host is not the address your phone can reach (for example a container or VPN interface), pairing will fail at the network layer even though everything else is correct.