Push (Server) Releases
1.0 TBD
Section titled “1.0 TBD”Initial release — provider-agnostic server-side push core: IPushManager orchestrator, IPushProvider
transport seam, IPushRepository persistence, IPushInterceptor pipeline, structured PushFilter
targeting, rich PushNotification model, automatic dead-token pruning + token rotation, bounded-concurrency
dispatch, and an in-memory repository + debug provider. AOT/trim-safe.
One package, four transports — APNs, FCM, Web Push and WNS all ship inside Shiny.Extensions.Push;
there are no separate *.Apns / *.Fcm / *.WebPush / *.Wns packages. Each keeps its own namespace
(Shiny.Extensions.Push.Apns / .Fcm / .WebPush / .Wns) and stays opt-in via AddApns / AddFcm /
AddWebPush / AddWns, so you only pay for what you register. Shiny.Extensions.Push.DocumentDb stays a
separate package (it pulls a third-party dependency). The implementation types you don’t inject directly
(PushManager, InMemoryPushRepository, DebugPushProvider, PushMetrics) live in the
Shiny.Extensions.Push.Infrastructure namespace — resolve the interfaces from the root namespace; you only
need the Infrastructure import to name a concrete type, e.g. AddProvider<DebugPushProvider>().
Direct APNs provider — HTTP/2 with token-based .p8 / ES256 JWT auth (no FCM dependency),
per-registration sandbox/production, cached provider JWT, pooled HTTP/2 connection, Apple-specific payload
options, and multi-app keyed registration via AddApns("key", …) + AppId.
WNS / Windows provider — delivers to Windows devices via WNS using the modern Windows App SDK /
Microsoft Entra (Azure AD) auth model (AddWns with TenantId / ClientId / ClientSecret; OAuth2
client-credentials token, cached). Sends a ToastGeneric toast by default (or a raw/tile/badge payload via
WindowsPushOptions) to the channel URI, maps 410/404 to dead-token pruning and 406/429 to
rate-limited, and supports multi-app keyed registration.
Shiny.DocumentDb persistence (Shiny.Extensions.Push.DocumentDb) — IPushRepository over any
DocumentDb backend with O(1) token-keyed writes; SQLite integration-tested.
Metrics — System.Diagnostics.Metrics counters (push.notifications.sent/.failed/.skipped,
push.tokens.pruned) and a push.send.duration histogram under the meter Shiny.Extensions.Push,
tagged by platform/provider/status. OpenTelemetry-ready.
Provider batching / FCM multicast — a new optional IPushBatchProvider capability lets a transport
deliver one notification to many devices in a single call. FcmProvider implements it (up to 500 devices
per multipart /batch request), so broadcasts and topic fan-out use far fewer round trips. Enabled by
default (PushManagerOptions.EnableBatching); devices are grouped by the identical notification instance,
so per-device localization falls back to per-device sends. Per-device pruning, token rotation, metrics, and
OnSent/OnFailed are all preserved.
Runtime configuration provider (dynamic / multi-tenant) — beyond the static AddApns(o => …)
registration, credentials can be supplied at send time through an IPushConfigurationProvider, keyed by
DeviceRegistration.AppId, so apps and keys can be onboarded, offboarded, or rotated without an app
restart. Register your provider once with UsePushConfiguration<T>() — it’s scoped, so it can depend on
scoped services like an EF DbContext — and opt each transport in with its no-argument overload
(AddApns() / AddFcm() / AddWebPush() / AddWns()). The library reuses each app’s minted APNs JWT /
OAuth bearer on the transport’s normal token lifetime and re-reads your current config on refresh, so a
rotated key is picked up within one window — no version bookkeeping. FCM batches are split per app. An
unknown/unconfigured AppId returns a failed result ("app not configured for …") rather than delivering.
Event receivers — a new IPushEventReceiver observes the send lifecycle without altering it (telemetry,
receipts, dead-letter capture, dashboards). Register zero or more with AddEventReceiver<T>(). Hooks:
OnBatchStarted, per-device OnSent/OnFailed, and OnBatchFinished (with the aggregate PushSendResult).
Unlike an IPushInterceptor it can’t skip or mutate; and OnFailed fires for every failed device —
including the normalized failures that never throw (TokenExpired, InvalidToken, RateLimited) — carrying
the full PushDeliveryResult. A receiver that throws is logged and swallowed, never breaking a batch.
Live Activities (ActivityKit) — Apple.LiveActivity turns any push into a Live Activity update: the
APNs transport switches to apns-push-type: liveactivity, the <bundle-id>.push-type.liveactivity topic,
and an aps body of event/timestamp/content-state. LiveActivityPush.Start/Update/End build the
three lifecycle pushes with their required fields enforced. Content state is typed via LiveActivityValue
(implicit conversions for string/int/long/double/bool/DateTimeOffset, plus Array/Object/Json)
because the widget’s Swift ContentState is a Codable struct — a number sent as a string silently fails
to decode. Dates default to Swift’s reference epoch (2001-01-01), overridable with
LiveActivityDateEncoding. Also supported: StaleDate, DismissalDate, RelevanceScore, alerting
updates, and push-to-start’s RequestPushToken/InputPushChannel.
Live Activity token safety — DeviceRegistration.TokenKind records whether a token is an ordinary
device token, a push-to-start token, or a per-activity update token, and PushFilter.TokenKind defaults to
Device. Without this a plain Broadcast would target Live Activity tokens, APNs would reject them with
DeviceTokenNotForTopic, and the manager would prune them as invalid. RegisterLiveActivityToken,
SendLiveActivity (which infers the kind from the event) and SendLiveActivityToTokens are the entry
points. One DeviceId can now hold a device token and its Live Activity tokens side by side.
APNs broadcast channels (iOS 18+) — ApnsBroadcastClient creates, reads, lists and deletes broadcast
channels and sends one Live Activity push to every subscriber, instead of one push per activity token.
Resolved from DI keyed by the same app key as AddApns.