block / block/buzz

iOS push alerts are silent — add sound to the NIP-PL APNs constant

Open
#7,186 1 comment 1 reaction 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.