Files
nucleic/docs/PUSH_SETUP.md
T
abkslmandClaude Fable 5 e93fc14c9b relay: host self-enrollment — no admin secret in the app; push is one toggle
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]>
2026-07-02 17:21:38 -07:00

8.1 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 — operator-only (revoke, manual smoke tests). Hosts never see it: each Mac self-enrolls for its own scoped credential (§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 fallback only — each device record carries its own environment (stamped by the host at registration), so mixed sandbox/production fleets work. Set production
  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 — 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 .p8 lets 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):

  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.