APNs
The APNs transport talks to Apple Push Notification service directly — HTTP/2 with token-based (.p8)
authentication. No FCM/Google dependency for Apple devices. It handles iOS and macOS registrations. It ships
in the Shiny.Extensions.Push package (namespace Shiny.Extensions.Push.Apns) — no separate package to
install; just add using Shiny.Extensions.Push.Apns; to reach AddApns.
Apple Developer setup
Section titled “Apple Developer setup”Requires a paid Apple Developer Program membership.
- Create an APNs auth key. Go to Certificates, Identifiers & Profiles → Keys, click +, name the key, tick Apple Push Notifications service (APNs) and register it. Download the
AuthKey_XXXXXXXXXX.p8file, which can only be downloaded once, and note the Key ID. - Find your Team ID under Membership details.
- Check the App ID. Under Identifiers, your App ID’s bundle id is
BundleId, and it must have the Push Notifications capability enabled.
One .p8 key works for every app on the team, in both sandbox and production. The app side (entitlements, background
mode, reporting the token) is covered in the client Platform Setup → Apple.
Configuration
Section titled “Configuration”You need the .p8 auth key, its Key ID, your Team ID, and the app’s bundle id.
push.AddApns(o =>{ o.TeamId = "ABCDE12345"; // 10-char Apple Team ID (JWT "iss") o.KeyId = "KEY1234567"; // 10-char Key ID of the .p8 (JWT "kid") o.BundleId = "com.example.app"; // default apns-topic o.PrivateKeyPath = "AuthKey_KEY1234567.p8"; // or o.PrivateKey = "<PEM contents>" // o.ForceEnvironment = PushEnvironment.Sandbox; // default: honour each registration});The provider builds and caches its ES256 provider JWT (refreshed ~every 50 minutes — Apple rejects tokens regenerated too frequently) and reuses a single pooled HTTP/2 connection across all sends.
Sandbox vs production
Section titled “Sandbox vs production”APNs tokens are environment-specific — a token registered against the sandbox (debug/TestFlight builds)
is not valid in production and vice-versa. Set DeviceRegistration.Environment per device:
await pushManager.RegisterDevice(new DeviceRegistration{ DeviceToken = "<token>", Platform = DevicePlatform.iOS, Environment = PushEnvironment.Sandbox});The provider routes each push to the correct host based on the registration. Use
ApnsOptions.ForceEnvironment only if you want to pin every push to one environment.
Apple-specific payload options
Section titled “Apple-specific payload options”ApplePushOptions on the notification maps to the aps dictionary and APNs headers:
new PushNotification{ Title = "Title", Apple = new ApplePushOptions { Subtitle = "Subtitle", Category = "MESSAGE_CATEGORY", // actionable notifications ThreadId = "thread-1", // grouping MutableContent = true, // Notification Service Extension ContentAvailable = false, // true => silent/background push // TopicOverride / PushTypeOverride for VoIP / complication topics }};Common cross-cutting fields also map to APNs: Badge, Sound, CollapseId (apns-collapse-id),
TimeToLive (apns-expiration), and Priority (10/5; forced to 5 for background pushes).
Failure handling
Section titled “Failure handling”The provider normalizes APNs responses to PushDeliveryStatus:
| APNs result | Status | Manager action |
|---|---|---|
410 Unregistered |
TokenExpired |
token pruned |
BadDeviceToken / DeviceTokenNotForTopic |
InvalidToken |
token pruned |
429 TooManyRequests |
RateLimited |
surfaced (no auto-retry) |
ExpiredProviderToken / InvalidProviderToken |
Error |
JWT cache invalidated |
Multiple apps
Section titled “Multiple apps”Register one keyed provider per bundle id — see Sending → Multiple apps. Each
key gets its own options, .p8 key, and JWT cache.


