Files
nucleic/docs/PUSH_SETUP.md
T
abkslmandClaude Fable 5 add50fe074 mac: direct APNS sender — the host can wake phones without the relay
Answering "can push be local when on LAN": a backgrounded iOS app can
only be woken through Apple's push service, so a LAN-only wake path
doesn't exist — but the *sender* can be this Mac. DirectAPNSSender
holds the APNS .p8 (path + Key ID + Team ID, runtime-configured like
the relay: env vars first, then the Settings-written defaults keys)
and posts the same content-free approval.pending tickle straight to
api.push.apple.com (ES256 provider JWT via CryptoKit, 40-min cache,
per-device throttle) — no Cloudflare dependency for push.

ApprovalPushing unifies the two senders; SyncHost wakes non-connected
paired devices through whichever is configured (direct wins). The
Settings ▸ Remote push section gains a sender picker with the direct
fields (.p8 chooser, Key ID, Team ID). PUSH_SETUP.md §4 rewritten as
Option A (this Mac) / Option B (relay). JWT signing verified against
the public key in tests; payload asserted byte-compatible with the
worker's apns.ts tickle.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-02 16:18:53 -07:00

8.4 KiB
Raw Blame History

Manual setup — finishing iOS push, Live Activities & the relay wake path

Everything code-side is built, tested, and merged to dev (notification pipeline, actionable approvals, Live Activity extension, host PushRelayClient, worker /v1/push/register). What remains is credentialed, account-level, or requires a human at a device — this runbook is that list, in order. Design context: CLOUD_INFRA.md §23, UX_IOS.md §5.

0. Re-sign today's commits

1Password was locked during the working session, so SSH commit signing failed and these landed unsigned on claude/competent-pasteur-a5bb90 (and via merges on dev):

  • ac17282 ios parity, 0bd531e sync wire projections, babd047 push/Live Activity, plus the two dev merge commits and the docs commit that follows.

Unlock 1Password, then re-sign the branch (from the worktree or any checkout of it):

git rebase --exec 'git commit --amend --no-edit -S' d385571   # base of the branch

(or leave them unsigned if you don't enforce signing on dev). Push when satisfied: git push origin dev claude/competent-pasteur-a5bb90.

1. Apple Developer portal — APNS auth key

  1. developer.apple.com ▸ Certificates, IDs & Profiles ▸ Keys+ → name it (e.g. Nucleic APNS), check Apple Push Notifications service (APNs), Continue, Register.
  2. Download the .p8 once (it can't be re-downloaded) and note the Key ID.
  3. Team ID is L7UDTQ6F5W.

⚠️ Bundle-id gotcha: CLOUD_INFRA.md and the wrangler comments say the topic is xyz.blakeslee.nucleic.remote, but the app actually builds as xyz.blakeslee.nucleic-remote (dash, not dot — see PRODUCT_BUNDLE_IDENTIFIER). The APNS topic must be the real bundle id or every push is rejected with BadTopic.

2. Xcode — Push Notifications capability (one click)

The entitlements file (aps-environment = development) is already wired into the build (CODE_SIGN_ENTITLEMENTS), and NSSupportsLiveActivities is set. What Xcode must do once, because it registers the capability on the App ID server-side:

  1. Open ios/NucleicRemote/NucleicRemote.xcodeproj ▸ target NucleicRemoteSigning & Capabilities+ CapabilityPush Notifications.
  2. Let automatic signing regenerate the provisioning profile (or run any xcodebuild … -allowProvisioningUpdates build, e.g. scripts/ios-release.sh --archive-only).
  3. The NucleicRemoteWidgets extension target needs no push capability — Live Activities ride the app's NSSupportsLiveActivities.

3. Cloudflare — provision & deploy nucleic-edge

Prereq: the blakeslee.xyz zone on the account (the custom-domain routes need it). All commands from cloud/nucleic-edge/ with wrangler authed (npx wrangler login, or CLOUDFLARE_API_TOKEN).

  1. KV namespaceswrangler.jsonc carries ids; if they aren't real on this account, create and paste the returned ids over the existing ones:

    npx wrangler kv namespace create NUCLEIC_RELAY_TOKENS
    npx wrangler kv namespace create NUCLEIC_PUSH_TOKENS
    
  2. Secrets (npx wrangler secret put <NAME> each):

    Secret Value
    RELAY_TOKEN_SECRET fresh random, e.g. openssl rand -hex 32
    RELAY_ADMIN_SECRET fresh random — the host presents this as a bearer; you'll reuse it in §4
    APNS_KEY_P8 the full PEM contents of the .p8 from §1
    APNS_KEY_ID the Key ID from §1
    APNS_TEAM_ID L7UDTQ6F5W
    APNS_TOPIC xyz.blakeslee.nucleic-remote (see the gotcha in §1)
    APNS_ENV sandbox to start (Xcode-installed dev builds). TestFlight/App Store builds use production — the worker picks one host globally, so flip this when you move to TestFlight testing; mixed fleets aren't supported yet
  3. Deploy & verify:

    npm ci && npm test && npm run check
    npm run deploy
    curl https://api.nucleic.blakeslee.xyz/health        # → ok
    
  4. Smoke-test the push path (token from a real device, visible in §5):

    curl -i -XPOST https://api.nucleic.blakeslee.xyz/v1/push/register \
      -H "authorization: Bearer $RELAY_ADMIN_SECRET" -H 'content-type: application/json' \
      -d '{"deviceId":"iphone-XXXX","token":"<apns hex token>","env":"sandbox"}'
    curl -i -XPOST https://api.nucleic.blakeslee.xyz/v1/push/notify \
      -H "authorization: Bearer $RELAY_ADMIN_SECRET" -H 'content-type: application/json' \
      -d '{"deviceId":"iphone-XXXX"}'
    # → 200 and the phone shows "A session is waiting for your approval"
    

Optional: authorize the Cloudflare connectors in claude.ai connector settings if you want agent sessions to be able to run this provisioning next time.

4. Mac host — choose a push sender

These are runtime settings on the host Mac, not build settings. They're read when the sync server starts — process environment first (useful for make run / CI), then the same-named UserDefaults keys the Settings UI writes. A Finder-launched app never sees shell env vars, so the normal path is Nucleic ▸ Settings ▸ Remote ▸ "Approval push", which offers two senders (pick either; both send the identical content-free tickle):

Option A — This Mac (direct APNS, no relay). The Mac holds the .p8 from §1 and talks to api.push.apple.com itself. No Cloudflare dependency for push at all; the push still transits Apple's servers — iOS gives a backgrounded app no other wake path, even on the same LAN. Fill in the key file, Key ID, and Team ID (env-var form: NUCLEIC_APNS_KEY_PATH, NUCLEIC_APNS_KEY_ID, NUCLEIC_APNS_TEAM_ID, optional NUCLEIC_APNS_TOPIC — defaults to the correct xyz.blakeslee.nucleic-remote).

Option B — Cloud relay. The relay holds the APNS key (§3 secrets) and this Mac asks it to wake devices. Fill in the relay URL (https://api.nucleic.blakeslee.xyz) and the RELAY_ADMIN_SECRET (env-var form: NUCLEIC_RELAY_URL, NUCLEIC_RELAY_ADMIN_SECRET). This is the path that will matter once full relay transport ships (phone reachable off-LAN); until then A and B are equivalent in effect.

Shared: the APNS environment picker (NUCLEIC_APNS_ENV) must match the phone build — sandbox for Xcode dev installs, production for TestFlight / App Store. Hit Apply (it restarts remote access). Push stays entirely off while the chosen sender's fields are blank; when both are configured, direct wins.

From then on: an approval arriving while a phone holds no live socket triggers the wake (throttled to one per device per 30s). With the relay sender, each phone's APNS token is also mirrored to the relay after its sync Hello; the direct sender needs no registration — the host already holds the token from the Hello.

5. On-device verification (needs a physical iPhone)

The simulator can't take this the last mile: the notifications permission dialog can't be accepted headlessly, and simulators don't get real APNS tokens. On a device (Xcode-run build):

  1. Launch → Allow the notifications prompt. Pair with the Mac.
  2. Local path (LAN): background the app, make an agent hit an approval on the Mac → banner with the session title + tool. Low-risk shows Allow/Deny on the banner (Allow asks for Face ID/passcode); destructive/network/host-exec shows no actions — tapping opens the in-app card with its Face ID gate. Answering on the Mac first withdraws the banner.
  3. Routing/badge: tapping a notification lands in that session; the app icon badge tracks the NEEDS YOU count.
  4. Live Activity: with a session running, the lock screen / Dynamic Island shows the aggregate "N running · M need you" activity; it updates as sessions change and disappears when everything is idle. (If nothing appears: iOS Settings ▸ NucleicRemote ▸ Live Activities.)
  5. Remote wake (relay): force-quit the app (or leave WiFi), trigger an approval on the Mac → the phone gets the localized approval.pending tickle via APNS; opening the app reconnects and shows the approval.

6. TestFlight

scripts/ios-release.sh now stamps the build channel (NUCLEIC_CHANNEL, default beta → the blue in-app banner; canary etc. work too). Remember from §3: TestFlight builds sign as aps-environment = production, so set the worker's APNS_ENV secret and the host's NUCLEIC_APNS_ENV to production when testing via TestFlight.