iOS push alerts are silent — add sound to the NIP-PL APNs constant
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Motivation**
The mobile app can wake on a NIP-PL push and show a banner, but the alert is silent. Operators using Buzz as a human–agent workspace need an audible cue when the phone is locked or the app is backgrounded — same expectation as Slack/iMessage, not a mute reconnect banner.
This is a product request from a multi-workspace operator (Litbox nest). It is **not** “please add push.” iOS push registration, lease publish, gateway delivery, and the Notification Service Extension already landed in [#6269](https://github.com/block/buzz/pull/6269) (`feat(mobile): push notifications MVP`, merged 2026-08-28). The remaining hole is sound.
**Why it is silent today**
NIP-PL’s public APNs profile is class-blind: every accepted delivery sends one compiled-in body. That body has no `sound` key.
Verified at `block/buzz` `bd734904`:
```rust
// crates/buzz-push-gateway/src/model.rs
pub const APNS_RECONNECT_PAYLOAD: &[u8] =
br#"{"aps":{"alert":{"body":"Reconnect to your relay now"},"mutable-content":1}}"#;
```
The same exact UTF-8 constant is pinned in:
- `docs/nips/NIP-PL.md:227`
- `docs/formal/nip-pl/delivery.py` (`FIXED_BODY`)
- `docs/formal/nip-pl/fixed_payload.py` (`C`)
Under APNs, an alert without `sound` displays a banner **without playing audio**. Client permission is already requested (`.alert, .badge, .sound` in `mobile/ios/Runner/AppDelegate.swift`). The NSE (`mobile/ios/NotificationService/NotificationService.swift`) copies the incoming `UNMutableNotificationContent` and never sets `.sound`. So neither the gateway payload nor the extension adds a sound — permission alone does not play one.
The gateway cannot vary the body per event. NIP-PL § Wake Delivery requires `application_body(a) = C_transport` for every send, and `DeliveryRequest` carries only `v`, `endpoint_grant`, `request_id`, `expires_at`. `silent` is a matching preference only; the public APNs profile does not expose relay-selected notification classes. Sound therefore has two legal levers: put it in the constant, or set it on-device in the NSE. The constant is the one that still works when the extension times out or fails.
**Proposed solution**
v1: system default sound on every public-profile APNs wake. No per-slot custom sounds.
Amend the four pins together to:
```json
{"aps":{"alert":{"body":"New activity in Buzz"},"sound":"default","mutable-content":1}}
```
Change the body text in the same edit. Without the NSE, this string is what every user sees on every push, and `"Reconnect to your relay now"` is internal plumbing language. `"New activity in Buzz"` (or maintainer-chosen copy) is a user-facing placeholder the NSE can still replace when it has relay data.
`crates/buzz-push-gateway/src/apns.rs` already asserts every sent body equals `APNS_RECONNECT_PAYLOAD`, so it follows the constant. No Desktop change. No Android work in this issue.
Acceptance:
- A lock-screen / background APNs wake on a physical iOS device plays the system default sound when notification permission includes sound.
- The four pins stay byte-identical to each other.
- Formal noninterference still holds: `application_body()` takes no arguments.
- NSE replacement path still runs; it may keep or set `UNNotificationSound.default` but must not depend on that for v1 audibility.
**Alternatives considered**
| Approach | Why not for v1 |
|---|---|
| NSE sets `content.sound` and leave the constant mute | Silent whenever the extension is not invoked, times out, or fails closed — the exact fallback path users hit. |
| Per-slot / per-channel custom sounds (Desktop local sounds) | Desktop’s per-slot sounds are for an **online** app. Background APNs cannot reuse that path; the public profile is class-blind by spec. |
| Silent data-only wake + local notification | Fails when the app is killed and iOS does not schedule the local notification. |
| Re-file “mobile never registers for push” | Already shipped in [#6269](https://github.com/block/buzz/pull/6269). [#4657](https://github.com/block/buzz/issues/4657) is the registration bug, now stale for iOS. |
**Non-goals**
- Android / FCM — already [#6092](https://github.com/block/buzz/issues/6092) (client) and [#3229](https://github.com/block/buzz/issues/3229) (gateway profile).
- Per-channel mute/level UI — [#3234](https://github.com/block/buzz/issues/3234) / [#3160](https://github.com/block/buzz/issues/3160).
- Self-hosted relays reaching `push.buzz.xyz` — [#5206](https://github.com/block/buzz/issues/5206).
- Custom sound files or notification categories.
**Additional context**
[#6269](https://github.com/block/buzz/pull/6269) validated lock-screen presentation and NSE replacement on a physical iPhone 12 mini; that write-up does not claim an audible alert. Apple provisioning (`.p8`, App Attest app id, production vs sandbox) remains an operator/release step and is not this issue.
**Duplicate check**
Searched 2026-09-01 for `push notification`, `alert sound`, `APNs`, `APNS_RECONNECT_PAYLOAD`. Closest, none of which ask for `sound` on the fixed APNs body:
- [#4657](https://github.com/block/buzz/issues/4657) — client never registers (iOS half landed in #6269)
- [#6092](https://github.com/block/buzz/issues/6092) / [#3229](https://github.com/block/buzz/issues/3229) — Android FCM
- [#6894](https://github.com/block/buzz/issues/6894) — huddle *request* alert, not message push
- [#3215](https://github.com/block/buzz/issues/3215) / [#2562](https://github.com/block/buzz/issues/2562) — Desktop sounds
- Open PR [#1924](https://github.com/block/buzz/pull/1924) — older iOS foundation; superseded for MVP by #6269
Contributor guide
Assessment
This issue has not been assessed yet.