DocumentationOperate

The paired app

Updated: 2026-09-25 19:25 CEST

Run canary app on the Mac to get Canary on your phone. That single process serves the progressive web app, handles pairing, streams live updates to paired devices over /api/events, and sends opt-in push notifications.

Pair a phone

canary app

In another shell:

canary app pair

Scan the QR code or open the printed pairing URL. The URL contains a short-lived pairing id plus nonce, not a durable secret. After the first successful pair, the browser keeps a device credential and the app stores a matching device grant under the app state directory.

On iOS, pair in Safari first and then Add to Home Screen, so the install snapshot carries the pairing cookie. The installed app's container inherits Safari's cookies but not Safari's localStorage or IndexedDB, so a key-based re-login can never run there.

The default app bind is LAN-capable (0.0.0.0:8765), so the same app process can serve a local browser preview at http://127.0.0.1:8765 and a phone at the Mac's LAN URL. Use plain canary app pair for the phone, so the running app host's configured public URL is used. For a local preview, override only the pairing URL:

canary app pair --public-url http://127.0.0.1:8765 --json

Do not run the shared app host with --addr 127.0.0.1:8765 when a phone needs to pair too.

Staying paired

Restarting canary app clears only the in-memory session cookie. The paired device mints a fresh session from its device grant silently, retries transient failures at page load, and shows the "scan a fresh QR code" instruction only when the app definitively rejected the device. A stale or already-consumed pairing URL is recoverable too: the app strips the one-time pairing query and falls through to the saved device credential before asking for a new QR code.

Session continuity does not depend on script-visible storage. Pairing sets a long-lived HttpOnly ibkr_app_device cookie whose hash is stored on the device grant, and the app mints fresh sessions from it. Key/secret logins re-provision the cookie, and grants keep a capped list of valid cookie hashes so Safari and the installed app (twin copies of the same cookie jar) never invalidate each other.

Every auth outcome is logged as canary app auth: lines in ~/Library/Logs/ibkr/app.err.log.

Check app and alert health

canary app status
canary app status --json

This local-Mac-only check proves the app HTTP host is reachable and reports the two alert authorities separately. alert_producer shows whether the app has a current, complete daemon-authored source snapshot. alert_dispatcher shows the app-owned delivery state and its fixed failure class, if any. A current active alert is not itself a health failure. push_delivery is the phone-push proof described below. Device grant ids, subscription endpoints and keys, occurrence identities, account data, and raw transport errors are not returned; devices appear only by their paired name and an opaque reference. The remote relay refuses this route.

Prove the phone receives pushes

The push service accepting a notification proves transport, not delivery. A push is witnessed only when a paired device reports it back: the app's service worker sends a displayed receipt as it shows the notification and an opened receipt when you tap it, and the page repeats the opened receipt when the tap launches the app. Receipts go to POST /api/push/ack, bound to the session's own device grant. The app journals every push per notice with its transport class and HTTP status, and every receipt with its device, and relays the latest facts to the daemon.

To prove the channel end to end, send a diagnostic push from the Mac:

canary app push-test

It goes to every device with notifications on and travels the alert transport, journal, and receipt path, but it is marked diagnostic: it never counts as an alert, never enters the inbox, and never counts toward the runaway fuse. Tap the "Canary notification test" notification on the phone, then run:

canary status

The Phone push row says how long alert pushes have been silent, which device last reported a push displayed or opened, and what blocks delivery: alert intake refused, no subscription, an expired subscription, or notifications off. canary status --json carries the full record under push_delivery, and canary app status adds the last push sent. /api/push/diagnostic is local-Mac only and the remote relay refuses it.

A phone that was paired again gets a new device grant, and the browser can still report "browser subscribed" while that grant has no push subscription. The phone's own Safe test then says the device is not subscribed: tap Enable in Settings and send the test again.

Remote access

Remote mode keeps the normal app host local, then opens an outbound connector to https://remote.osauer.dev:

canary app --remote

In another shell:

canary app pair

The printed pairing URL uses the relay origin and includes a remote= route id.

Remote mode needs the Mac, TWS/Gateway, canary app --remote, and the Cloudflare Worker route to stay up. The app exposes relay connection state in /api/bootstrap under relay.

What survives a restart

The route id and connector token are persisted locally and resumed across app restarts, new builds, and relay-side route expiry. A token-matched resume revives the route at the relay, so the route id, and with it every paired phone, survives arbitrary Mac downtime as long as the app state directory keeps state.json. Registration happens inside the connector loop with backoff, so a relay or DNS outage at startup degrades the relay instead of killing canary app --remote.

A new route id is minted only when the relay definitively rejects the held credentials, and the app logs that paired devices must re-pair.

What the phone sees when something is wrong

Phone-side addressing is double-tracked. The relay sets the ibkr_remote_route cookie with a 400-day Max-Age and refreshes it on every visit, and the app mirrors the route id in localStorage. A navigation that arrives without the cookie gets a recovery page that rebuilds it from the mirror. A navigation while the Mac connector is down gets an auto-retrying wait page instead of raw JSON.

Running remote mode under launchd

For an unsupervised app host, an app-only restart can switch remote mode on:

canary restart --app --remote

Do not pass that override to a loaded LaunchAgent: supervised restart rejects runtime flag overrides because the loaded plist is authoritative. Install a new supervised host in remote mode with:

canary setup app --remote
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.osauer.ibkr-app.plist

To change an already-loaded LaunchAgent, run canary setup app --remote, unload the existing gui/$(id -u)/com.osauer.ibkr-app job, and bootstrap the rewritten plist again. Ordinary later restarts use canary restart --app with no --remote override; launchd preserves the plist configuration.

Restart

canary restart

On the default daemon socket, this restarts a running app first and only then restarts the shared daemon. If no app is running, plain canary restart leaves the app stopped.

The two stages are ordered but not atomic. An app-stage failure leaves the daemon untouched; a daemon-stage failure does not roll the already-restarted app back. Fix the reported stage and rerun canary restart. Updating has the flags, JSON output, and socket-scope rules.

When the com.osauer.ibkr-app LaunchAgent is loaded, the app is restarted through launchctl kickstart -k. Any unsupervised (orphaned) app process is stopped first so launchd can own the app again, and app flag overrides are rejected with a pointer to rewrite and reload canary setup app because the plist arguments win. The loaded job's executable must resolve to the current installed canary binary. launchd may throttle the respawn for about ten seconds; the command waits for a stable supervised PID.

Without a loaded LaunchAgent, the unsupervised behavior applies: SIGTERM the running app, preserve its flags such as --addr, --public-url, --remote, and --state-dir, and start a detached replacement.

Use canary restart --app for app-only restart/start workflows, including cases where no app is running yet. canary app restart is an alias for the same thing.

To switch an old local-only app host back to the shared local-preview plus phone mode:

canary restart --app --addr 0.0.0.0:8765

When --addr is overridden without --public-url, restart clears any preserved app --public-url flag so the app can derive the Mac's LAN URL again.

Developing the app

Building or debugging the app itself is contributor work. Canary SPA Development has the development and debugging playbook, including P/L semantics and Browser plugin caveats, and make help lists the app refresh and browser-smoke targets.