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]>
8.4 KiB
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 §2–3, 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):
ac17282ios parity,0bd531esync wire projections,babd047push/Live Activity, plus the twodevmerge 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
- developer.apple.com ▸ Certificates, IDs & Profiles ▸
Keys ▸ + → name it (e.g.
Nucleic APNS), check Apple Push Notifications service (APNs), Continue, Register. - Download the
.p8once (it can't be re-downloaded) and note the Key ID. - 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 asxyz.blakeslee.nucleic-remote(dash, not dot — seePRODUCT_BUNDLE_IDENTIFIER). The APNS topic must be the real bundle id or every push is rejected withBadTopic.
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:
- Open
ios/NucleicRemote/NucleicRemote.xcodeproj▸ target NucleicRemote ▸ Signing & Capabilities ▸ + Capability ▸ Push Notifications. - Let automatic signing regenerate the provisioning profile (or run any
xcodebuild … -allowProvisioningUpdatesbuild, e.g.scripts/ios-release.sh --archive-only). - 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).
-
KV namespaces —
wrangler.jsonccarries 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 -
Secrets (
npx wrangler secret put <NAME>each):Secret Value RELAY_TOKEN_SECRETfresh random, e.g. openssl rand -hex 32RELAY_ADMIN_SECRETfresh random — the host presents this as a bearer; you'll reuse it in §4 APNS_KEY_P8the full PEM contents of the .p8from §1APNS_KEY_IDthe Key ID from §1 APNS_TEAM_IDL7UDTQ6F5WAPNS_TOPICxyz.blakeslee.nucleic-remote(see the gotcha in §1)APNS_ENVsandboxto start (Xcode-installed dev builds). TestFlight/App Store builds useproduction— the worker picks one host globally, so flip this when you move to TestFlight testing; mixed fleets aren't supported yet -
Deploy & verify:
npm ci && npm test && npm run check npm run deploy curl https://api.nucleic.blakeslee.xyz/health # → ok -
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):
- Launch → Allow the notifications prompt. Pair with the Mac.
- 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.
- Routing/badge: tapping a notification lands in that session; the app icon badge tracks the NEEDS YOU count.
- 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.)
- Remote wake (relay): force-quit the app (or leave Wi‑Fi), trigger an approval on the
Mac → the phone gets the localized
approval.pendingtickle 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.