A small service that relays WebPush (RFC 8030) notifications to Apple's Push Notification service. It is wire-compatible with the original Ruby feditext-apns (itself a fork of Metabolist's metatext-apns), so existing Mastodon push subscriptions and the Feditext iOS notification service extension keep working.
This is a Rust port of feditext-apns. The endpoints, URL shape, and APNS payload JSON are unchanged. What's different is what's inside: a single Tokio process multiplexing requests over one HTTP/2 connection per environment instead of a multi-process / multi-thread Ruby server with a connection pool.
GET /—200. Health check.POST /push/{device_token}/{id}?sandbox={true|false}— relay a WebPush message.device_token: Base16 device token fromapplication(_:didRegisterForRemoteNotificationsWithDeviceToken:).id: opaque account identifier (echoed back to the client asiin the payload).sandbox=trueroutes to APNS development; otherwise production.- Request body: the encrypted WebPush ciphertext, opaque to this service.
Content-Encodingmust beaes128gcmoraesgcm; anything else →415.- For
aesgcm, theEncryption: salt=...andCrypto-Key: dh=...;...headers are parsed and forwarded assandk. - Always returns
202immediately. APNS errors are logged, not surfaced.
The APNS payload sent to Apple includes:
aps.alert = { "loc-key": "apns-default-message" }aps.mutable-content = 1(so the iOS notification service extension fires)- Custom keys at the top level:
e(encoding),i(account id),m(URL-safe Base64 of the ciphertext), and foraesgcmalsos(salt) andk(server public key from thedh=parameter). - HTTP header
apns-push-type: alert.
All configuration is via environment variables. The service exits at startup if any required variable is missing.
| Variable | Required | Notes |
|---|---|---|
APNS_AUTH_KEY |
yes | PEM contents of the .p8 private key from the Apple Developer portal (literal newlines, not \n escapes). |
APNS_KEY_ID |
yes | Key ID from the Apple Developer portal. |
APPLE_TEAM_ID |
yes | Team ID from the Apple Developer account. |
TOPIC |
yes | The app's bundle ID, sent as the apns-topic header. |
PORT |
no | TCP port to bind. Default 3000. |
RUST_LOG |
no | tracing-subscriber filter directive. Default info. |
The Ruby-era variables CONNECTION_POOL_SIZE, WEB_CONCURRENCY, MAX_THREADS,
and RACK_ENV are not used. HTTP/2 multiplexes many concurrent notifications over
a single connection per environment, so there is nothing equivalent to tune.
- APNS root CA rotated to USERTrust RSA (sandbox 2025-01-21, production
2025-02-24). This binary uses
rustlswithwebpki-roots, which already trusts USERTrust, so no action is required. If you switch the TLS backend, make sure your trust store includes the new root. - Team-scoped and topic-specific keys (introduced February 2025) work
transparently — generate them in the developer portal and drop them into
APNS_AUTH_KEY.
cargo run --releaseThe binary listens on 0.0.0.0:$PORT. Put it behind a reverse proxy if you need
TLS termination or rate limiting. The service has no authentication of its own —
it assumes whoever can reach it is trusted to send pushes (the same trust model as
the original Ruby version).
Graceful shutdown on SIGINT (Ctrl-C) drains the listener.
Licensed under the GNU Affero General Public License, version 3 or later
(AGPL-3.0-or-later). See LICENSE for the full text.
This is a derivative work of feditext-apns, which is itself derived from Metabolist's metatext-apns (Copyright © 2021 Metabolist). The AGPL terms are inherited from those upstream projects.