# 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](CLOUD_INFRA.md) §2–3, [UX_IOS.md](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):

```sh
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](https://developer.apple.com/account) ▸ 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 **NucleicRemote** ▸
   **Signing & Capabilities** ▸ **+ Capability** ▸ **Push 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 namespaces** — `wrangler.jsonc` carries ids; if they aren't real on this account, create
   and paste the returned ids over the existing ones:

   ```sh
   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:**

   ```sh
   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):

   ```sh
   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 Wi‑Fi), 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.
