The admin bearer must not ship in a distributed build, so hosts no longer use it at all. A Mac self-enrolls with the relay on first use (POST /v1/host/enroll) and receives its own scoped credential (only the SHA-256 is stored server-side; the credential lives in the login Keychain). Register and notify are now authorized per host: a host can wake only devices it registered itself (admin remains an operator-only override); room-registered records are owner-tagged with the DO id and refused out-of-band. Each device record's APNS environment now wins over the global APNS_ENV secret, so mixed sandbox/production fleets work. Settings ▸ Remote's section is now just "Relay" with a single "Push notifications" toggle that takes effect immediately; the relay URL and APNS-environment overrides appear only in local dev builds. PushRelayConfig reduces to enabled + baseURL (built-in production default) + apnsEnv (production default). PUSH_SETUP.md §3/§4 updated; stale dotted-bundle-id comments in env.ts/wrangler.jsonc fixed. 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 — operator-only (revoke, manual smoke tests). Hosts never see it: each Mac self-enrolls for its own scoped credential (§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_ENVfallback only — each device record carries its own environment (stamped by the host at registration), so mixed sandbox/production fleets work. Set production -
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 — flip the toggle
Nucleic ▸ Settings ▸ Remote ▸ Relay ▸ "Push notifications" — that's the whole setup. No
secrets, no URLs: on first use the host self-enrolls with the relay
(POST /v1/host/enroll) and receives its own scoped credential, kept in this Mac's login
Keychain. The credential can only register and wake devices this host paired — so nothing
privileged ships in the app, and one Mac can never push to another Mac's phones. The
RELAY_ADMIN_SECRET is operator-only (revocation, manual smoke tests) and is never entered
into the app.
Local development builds additionally show a Relay URL field and an APNS environment
picker (point at wrangler dev, or flip to sandbox for Xcode-installed phone builds).
Env-var overrides for scripted runs: NUCLEIC_RELAY_ENABLED=1, NUCLEIC_RELAY_URL,
NUCLEIC_APNS_ENV. Distributed builds use the production relay and production APNS —
exactly what the toggle configures.
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 — with a credential scoped to its own devices.
From then on: each phone's APNS token is mirrored to the relay after its sync Hello (owner- tagged to this host), 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.