A small, stateless Go service that emulates an RFC 8030 WebPush endpoint and
forwards the still-encrypted payload to APNs (iOS) and FCM (Android). It
exists so that concrnt's WebPush-only notification delivery
(concrnt/internal/worker/NotificationReactor.go) can reach native mobile
apps that can't speak WebPush directly.
The relay never decrypts anything. Decryption (RFC 8291 aes128gcm) happens
on-device: an iOS Notification Service Extension and an Android
FirebaseMessagingService hold the subscription's private key and auth
secret and decrypt locally.
POST /apns/:environment/:device_token—environmentisproduction,sandbox, ordevelopment(alias forsandbox).POST /fcm/:registration_tokenGET /health
A client builds its PushSubscription.endpoint pointing at one of the first
two routes; whatever WebPush sender POSTs to it gets relayed unchanged.
See config.example.yaml. Config is loaded from the
path in WEBPUSH_RELAY_CONFIG (default /etc/webpush-relay/config), which
may be a single file or a directory of files that get merged together.
APNs uses token-based (JWT) auth via a .p8 key — not the legacy p12
certificate — so a single key authenticates against both the production and
sandbox APNs hosts; the environment is selected per-request from the URL, not
from config.
- 4KB payload limit. Both APNs and FCM cap a message's payload/data size
at 4096 bytes. base64url-encoding the encrypted WebPush body adds ~33%
overhead, and the JSON envelope adds a little more, so the effective
encrypted-body ceiling is roughly 2.9KB. concrnt's push payload is a full
concrnt.Event(which can embed referenced documents), so larger events can exceed this. When a built payload would be too large, the relay falls back to sending a notification carrying no encrypted body (pomitted); the device shows a generic notification instead of failing silently. A permanent fix (trimming whatNotificationReactorsends) is out of scope for this repository. - No subscription cleanup. The relay is stateless and has no way to tell
concrnt to drop a dead subscription. It does propagate
410 Gonewhen APNs or FCM report a token as unregistered/invalid, which the WebPush sender could act on — but concrnt'sNotificationReactorcurrently only logs non-201 responses. Actual cleanup needs a concrnt-side change. - No VAPID validation. Like Mastodon's
webpush-apn-relay(the reference implementation this project follows the style of, but does not vendor), the relay ignores the WebPushAuthorization/VAPID headers. The device token or registration token embedded in the URL is treated as an unguessable capability.
go build ./...
go vet ./...
go test ./...Local smoke test without real APNs/FCM credentials — with a provider left
unconfigured, its route replies 503, so you can exercise request parsing
end to end:
go run ./cmd/webpush-relay &
curl -i -X POST \
-H 'Content-Encoding: aes128gcm' -H 'TTL: 30' -H 'Urgency: high' \
--data-binary @sample-encrypted-body.bin \
http://localhost:8000/fcm/some-tokenSet fcm.dryRun: true to validate real FCM registration tokens against
Google without actually delivering a notification.