feat(push): add a UnifiedPush transport profile to the NIP-PL push gateway (Google-free Android, self-host friendly)
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
Add an optional **UnifiedPush** transport profile to `buzz-push-gateway`, so Android clients can receive NIP-PL wakes without any Google dependency and self-hosted relay operators can deliver wakes through a distributor they own (e.g. a self-hosted ntfy instance).
This mirrors #3229 (FCM transport profile): a new transport on the existing NIP-PL foundation — same lease lifecycle, executor key custody, replay protection, quota, and delivery-authority model — rather than a downstream push architecture.
## Motivation
NIP-PL already reserves UnifiedPush as an optional transport (`"transport": "apns" | "fcm" | "unifiedpush"`) and states exactly what a future profile must add:
> A future profile requires a separately registered constant body and hostile-endpoint analysis.
Everything else is already transport-agnostic: the relay-side matcher (`crates/buzz-relay/src/push_runtime.rs`) matches leases and wakes without caring about the transport, and the gateway already abstracts senders behind the `PushTransport` trait (`crates/buzz-push-gateway/src/apns.rs`).
UnifiedPush is the only Android transport that keeps a self-hosted Buzz deployment fully Google-free, and it composes naturally with the wake-only model: the distributor delivers a fixed body, the client reconnects and fetches authoritative events over normal `REQ`. No event content, no relay-supplied bytes ever transit the transport — the same noninterference property the APNs profile enforces with its byte-exact APNs body.
## Proposed profile
### Registered constant body
Analogous to the APNs constant, define one gateway-owned UTF-8 constant that the gateway sends for every accepted UnifiedPush attempt, e.g.:
```json
{"v":1,"t":"reconnect"}
```
The exact bytes are maintainers' choice — the requirement is a single fixed constant, never containing relay-derived, lease-derived, or request-derived data. The distributor forwards it as-is to the client's UnifiedPush receiver; the client treats any wake the same way it treats an APNs wake: reconnect and `REQ`.
### Hostile-endpoint analysis
The UnifiedPush `endpoint` is a full HTTPS URL chosen by the distributor (typically `https:///up?up=...`), which is the core of the NIP's v1 objection. Proposed mitigations:
- **No sensitive data at risk**: the gateway POSTs only the registered constant body. No lease payload, no tokens, no NIP-98 auth headers toward the endpoint (contrast with relay→gateway auth). A hostile endpoint learns nothing except "some Buzz installation woke".
- **Scheme pinning**: `https` only; redirects not followed.
- **Optional per-profile endpoint allowlist**: public-gateway deployments keep the profile disabled (or pin it to their own distributor); self-hosted operators configure exactly their ntfy base URL. This also contains SSRF exposure from the gateway process.
- Reuse existing attempt/retry classification and `max_endpoint_len` bounds (4096 is ample for UP endpoints).
### Gateway and relay surface
- `crates/buzz-push-gateway`: new `unifiedpush.rs` implementing the existing `PushTransport` trait (mirroring `apns.rs`), route `/v1/deliveries/unifiedpush`, new `AppProfile` (e.g. `buzz-android-up`).
- `crates/buzz-relay/src/nip11.rs`: the default descriptor currently hard-codes a single APNs profile (`"app_profiles": [{"id": "buzz-ios-dogfood", "transport": "apns"}]`); make `app_profiles` relay-configurable so self-hosted operators can advertise their UP profile, and extend `class_support` for the UP profile's classes.
- Lease validation (`crates/buzz-relay/src/handlers/push_lease.rs`) already matches `app_profile` against the advertised descriptor and checks `transport` equality, so no schema change is needed there.
Client-side work (Android Flutter app: `unified_push` plugin, distributor handshake, publishing a `kind:30350` lease with the UP endpoint) belongs to the Android umbrella in #6092; this issue covers only the gateway/NIP-PL profile.
### Opt-in scoping
Keep the profile inert unless the relay advertises a UP-capable `app_profile`: public-gateway behavior for existing iOS profiles stays byte-identical, while self-hosted relays ("a relay you own") get Google-free Android push immediately.
## Alternatives considered
- **FCM (#3229)**: covers Android at scale, but requires Google services on both client and gateway; unsuitable for self-hosted, Google-free deployments.
- **Background polling / reconnect**: #4402 shows the background path is unreliable; polling is a battery/latency dead end.
- **Downstream bridge** (separate proxy watching the relay and firing distributor requests directly): duplicates security-sensitive infrastructure outside the NIP-PL lease/revocation/quota model — rejected for the same reasons #3229 lists.
## Questions
- Would a PR that keeps the profile disabled until `app_profiles` advertises a UP profile be acceptable?
- Preference on the registered constant body bytes and the `AppProfile` id naming (`buzz-android-up`)?
Contributor guide
Research direction
Start with crates/buzz-push-gateway/src/apns.rs and the PushTransport trait, then inspect crates/buzz-relay/src/nip11.rs and the existing push lease validation. Done means a disabled-by-default UnifiedPush profile has its route and fixed body, while relay descriptors can advertise configured UP profiles without changing existing APNs behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100