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.:
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://<ntfy-host>/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
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)?
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: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 thePushTransporttrait (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.:
{"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
endpointis a full HTTPS URL chosen by the distributor (typicallyhttps://<ntfy-host>/up?up=...), which is the core of the NIP's v1 objection. Proposed mitigations:httpsonly; redirects not followed.max_endpoint_lenbounds (4096 is ample for UP endpoints).Gateway and relay surface
crates/buzz-push-gateway: newunifiedpush.rsimplementing the existingPushTransporttrait (mirroringapns.rs), route/v1/deliveries/unifiedpush, newAppProfile(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"}]); makeapp_profilesrelay-configurable so self-hosted operators can advertise their UP profile, and extendclass_supportfor the UP profile's classes.crates/buzz-relay/src/handlers/push_lease.rs) already matchesapp_profileagainst the advertised descriptor and checkstransportequality, so no schema change is needed there.Client-side work (Android Flutter app:
unified_pushplugin, distributor handshake, publishing akind:30350lease 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
Questions
app_profilesadvertises a UP profile be acceptable?AppProfileid naming (buzz-android-up)?