An app can't hold a secret the machine's owner can't extract (root + debugger defeats bundling, Keychain, and obfuscation; the Secure Enclave can't import external keys), and a leaked .p8 can push to every user of the topic. So the push path is relay-only again: the key lives exclusively in the relay's Worker secrets and the Mac only asks the relay to send. Settings and PUSH_SETUP.md revert to the relay-only form, with the rationale recorded in §4. Co-Authored-By: Claude Fable 5 <[email protected]>
8.1 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 — point it at the relay
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 the in-app UI:
Nucleic ▸ Settings ▸ Remote ▸ "Approval push (relay)" — enter the relay URL
(https://api.nucleic.blakeslee.xyz), the RELAY_ADMIN_SECRET from §3, pick the APNS
environment (sandbox = Xcode dev installs, production = TestFlight / App Store), and hit
Apply (it restarts remote access so the new config takes effect). Push stays entirely
off while URL or secret is blank. Env-var form for scripted runs: NUCLEIC_RELAY_URL,
NUCLEIC_RELAY_ADMIN_SECRET, NUCLEIC_APNS_ENV.
Why relay-only (no "send from this Mac" mode): the APNS provider key must never live on a user's machine — an app can't hold a secret the machine's owner can't extract (root + a debugger defeats bundling, the Keychain, and obfuscation alike; the Secure Enclave can't import external keys). A leaked
.p8lets anyone push to every user of the topic. So the key lives exclusively in the relay's Worker secrets, and the Mac only ever asks the relay to send. (Note the admin secret has the same client-side character — fine for your own Macs, but a distributed build should move to host-scoped relay tokens, per CLOUD_INFRA §2.1, before shipping relay config to anyone else.)
From then on: each phone's APNS token is mirrored to the relay after its sync Hello, and an approval arriving while a phone holds no live socket triggers a content-free wake (throttled to one per device per 30s).
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.