Pyric
Navigate

pyric messaging compatibility matrix

100% of the public API supported

13 of 13 public API

Status legend

Conforming — sandbox matches prod, locked by a passing probe Not implemented yet — deliberately or pending Unverified — a target with a derived failing test, not a guarantee

firebase/messaging (client)

getMessaging(app?): MessagingReturns the FCM Messaging instance associated with the given (or default) FirebaseApp. Bound to the client component registered under the name messaging.
Upstream typings/JSDoc (firebase 12.13.0, @firebase/messaging 0.12.26); in-process mirror suite plus canonical-import SharedWorker replay messaging-app-boundary.pw.ts. (Structured evidence: oracle-conformance.test.ts)
getToken(messaging, options?): Promise<string>Subscribes the instance to push and resolves with an FCM registration token; requests notification permission if not already granted and rejects if denied. Production tokens are colon-separated, URL-safe, ~142 chars, with the suffix after the colon beginning APA91b, and are stable across repeated getToken calls on the same service-worker registration (no per-call rotation).
oracle: messaging-web-token-shape.json (minted, length 142, colon-separated, suffix starts APA91b, URL-safe) + messaging-web-token-stability.json (second getToken on the same registration returns the same token). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
deleteToken(messaging): Promise<boolean>Deletes the registration token and unsubscribes the instance from its push subscription; resolves truthy. After deletion no message reaches the client on either route, and a server send to the now-dead token eventually surfaces the UNREGISTERED / 404-class error on the send plane (propagation is asynchronous — the first send after delete may still be accepted while delivery has already stopped).
oracle: messaging-web-deletetoken-unregistered.json (deleteToken resolved truthy; no delivery to client; send plane eventually UNREGISTERED). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
onMessage(messaging, nextOrObserver): UnsubscribeDispatched with the push payload when a message arrives while a window client is visible; the returned function stops listening. Routing keys on page VISIBILITY, not focus: a visibilityState: "visible" page receives onMessage even when unfocused, and when no window client is visible the message routes to the service-worker onBackgroundMessage instead.
oracle: messaging-web-onmessage-foreground.json (focused page → onMessage) + messaging-web-visibility-routing.json (visible → onMessage, no visible client → onBackgroundMessage). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
isSupported(): Promise<boolean>Resolves whether every API required by FCM exists in the current browser window context (bound to isWindowSupported).
Upstream typings/JSDoc (@firebase/messaging 0.12.26); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface Messaging { app }Public interface of the FCM client SDK; exposes the bound FirebaseApp as app.
Upstream typings (@firebase/messaging 0.12.26 public-types); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface GetTokenOptions { vapidKey?; serviceWorkerRegistration? }Options for getToken: an optional vapidKey (Web Push certificate public key) and an optional serviceWorkerRegistration.
Upstream typings (@firebase/messaging 0.12.26 public-types); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface MessagePayload { notification?; data?; fcmOptions?; from; collapseKey; messageId }Received message envelope delivered to onMessage / onBackgroundMessage. Production deliveries carry top-level keys data, from, messageId, and notification; from equals the project messaging sender id and messageId is present. (from, collapseKey, messageId are typed as required.)
oracle: messaging-web-onmessage-foreground.json + messaging-web-onbackgroundmessage.json (top-level keys data/from/messageId/notification; from = sender id; messageId present). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface NotificationPayload { title?; body?; image?; icon? }Display-notification block inside a MessagePayload. Production foreground deliveries carry a notification object whose keys include title and body.
oracle: messaging-web-onmessage-foreground.json (notificationKeys body, title). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface FcmOptions { link?; analyticsLabel? }WebpushFcmOptions-style options carried on a client MessagePayload (link, analyticsLabel).
Upstream typings (@firebase/messaging 0.12.26 public-types); no observation yet — type-only shape closed by the assignability census (resolved decision #5).
NextFn, Observer, Unsubscribe (re-exported from @firebase/util)The callback, observer, and teardown types consumed by onMessage / onBackgroundMessage are re-exported from @firebase/util.
Upstream typings (@firebase/messaging 0.12.26 re-exports); no observation yet. (Structured evidence: oracle-conformance.test.ts)
const enum ErrorCode (client)Client failures surface as thrown FirebaseErrors carrying one of the 18 documented ErrorCode values (missing-app-config-values, only-available-in-window, only-available-in-sw, permission-default, permission-blocked, unsupported-browser, indexed-db-unsupported, failed-service-worker-registration, token-subscribe-failed, token-subscribe-no-token, token-unsubscribe-failed, token-update-failed, token-update-no-token, invalid-bg-handler, use-sw-after-get-token, invalid-sw-registration, use-vapid-key-after-get-token, invalid-vapid-key). The enum itself is @internal, not a public export.
Upstream typings (@firebase/messaging 0.12.26 src/util/errors); no client-error observation among the committed set. (Structured evidence: oracle-conformance.test.ts)

firebase/messaging/sw (service worker)

getMessaging(app?): Messaging (sw)Returns the FCM instance within a service-worker context (bound to getMessagingInSw); registers under the component name messaging-sw.
Upstream typings (@firebase/messaging 0.12.26 sw/index-public) plus real module-ServiceWorker served-entry replay messaging-app-boundary.pw.ts. (Structured evidence: oracle-conformance.test.ts)
onBackgroundMessage(messaging, nextOrObserver): UnsubscribeCalled when a message arrives while the app has no visible window client. Production routes background deliveries here rather than to onMessage; the delivered payload carries data / from / messageId and, for notification messages, a notification block. A DATA-ONLY message still fires onBackgroundMessage with no notification key, and a registered handler suppresses the SDK auto-display.
oracle: messaging-web-onbackgroundmessage.json (no visible client → onBackgroundMessage) + messaging-web-visibility-routing.json + messaging-web-data-only-background.json (data-only fires, no notification key). Replayed by the conformance suite and by a real module Service Worker connected to the canonical SharedWorker broker in messaging-app-boundary.pw.ts. (Structured evidence: oracle-conformance.test.ts)
experimentalSetDeliveryMetricsExportedToBigQueryEnabled(messaging, enable): voidEnables or disables delivery-metrics export to BigQuery at runtime; default off.
Upstream typings (@firebase/messaging 0.12.26 sw/index-public); no observation yet. (Structured evidence: oracle-conformance.test.ts)
isSupported(): Promise<boolean> (sw)Resolves whether every API required by FCM exists within the service-worker context (bound to isSwSupported).
Upstream typings (@firebase/messaging 0.12.26 sw/index-public); no observation yet. (Structured evidence: oracle-conformance.test.ts)
firebase/messaging/sw module boundary + shared type parityThe sw entry exports onBackgroundMessage, getMessaging, experimentalSetDeliveryMetricsExportedToBigQueryEnabled, and isSupported, but NOT getToken / deleteToken / onMessage; the client entry exports the latter but NOT onBackgroundMessage / the metrics toggle. The two modules register under different component names (messaging vs messaging-sw) and re-export identical Messaging / GetTokenOptions / MessagePayload / NotificationPayload / FcmOptions type declarations.
Upstream typings (@firebase/messaging 0.12.26 index.d.ts / index.sw.d.ts) plus Window and real module-ServiceWorker boundary replay messaging-app-boundary.pw.ts. (Structured evidence: oracle-conformance.test.ts)

Behavior classes over time & across clients (client receive plane)

Multi-tab delivery routing (several window clients, one shared service worker)When several window clients (browser tabs) of one origin share a single service-worker registration, a foreground delivery reaches every visible tab through onMessage, and when no tab is visible the one shared service worker receives the message once through onBackgroundMessage (the worker is shared across tabs, not instantiated per tab). A notification click focuses a single existing client rather than opening a duplicate. Stated from the FCM web SDK and the Service Worker client model; no committed observation replays the multi-tab fan-out yet.
Upstream documentation (@firebase/messaging 0.12.26 + W3C Service Worker Clients model); no observation yet. Candidate probe: open two tabs on one registration and deliver with one tab visible, then with both hidden — assert onMessage fires once per visible tab and onBackgroundMessage fires exactly once on the shared worker.
Multi-device delivery (one app instance holding several tokens; per-token fan-out)A single user or app can hold several registration tokens at once — one per browser, profile, or device. A token-targeted send reaches exactly one token; a topic, condition, or multicast send fans out independently to every subscribed token, with no server-side de-duplication across a user’s devices, so each device receives its own copy. Stated from the FCM targeting model; no committed observation replays cross-device fan-out yet.
Upstream documentation (FCM targeting model); no observation yet. Candidate probe: mint two distinct tokens, subscribe both to one topic, send once, and assert each token receives the message independently.
Registration-token rotation and refresh over the app lifecycleA registration token is stable within a session (row messaging#2 pins that repeated getToken calls on one service-worker registration return the same token) but is not permanent across the app lifecycle: the SDK rotates it on events such as app reinstall, SDK upgrade, a restored backup, or a notification-permission change, and Google may retire a token server-side. The modular web SDK has no token-refresh callback, so an app observes a new token only by calling getToken again; a rotated-away old token eventually stops delivering and the send plane reports the dead token (the UNREGISTERED path pinned by messaging#3). This lifecycle rotation is deliberately outside the within-session stability observation. Stated from FCM token-management documentation; no committed observation replays rotation across lifecycle events yet.
Upstream documentation (FCM token-management guidance); the committed messaging-web-token-stability observation covers only within-session stability, not lifecycle rotation. Candidate probe (long-horizon): force a service-worker unregister/re-register or an app-instance reset, re-mint via getToken, and record whether the token changed and whether the prior token then reports UNREGISTERED on send.
Registration-token inactivity expiry (server-side staleness window)FCM treats a registration token that has gone unused for an extended period (Google documents roughly 270 days of app-instance inactivity) as stale and drops it from delivery targeting, independent of any explicit deleteToken. The expiry clock is Google server-side inactivity accounting, and the recommended mitigation is periodic token refresh. Stated from FCM best-practices documentation; the sandbox has no server-side inactivity clock to model.
Upstream documentation (FCM registration-token best practices); no observation. This time-based server-side behavior is out of sandbox scope (see the row disposition); the explicit-deletion path to a dead token is already tracked by messaging#3.

firebase-admin/messaging — entry + Messaging class

getMessaging(app?): MessagingReturns the Messaging service for the default or given admin App.
Upstream typings/JSDoc (firebase-admin 13.10.0 lib/messaging/index); no observation yet. (Structured evidence: oracle-conformance.test.ts)
messaging(app?): messaging.MessagingNamespaced / legacy accessor equivalent of getMessaging, exposed by the compat entry alongside the namespace messaging type aliases.
Upstream typings (firebase-admin 13.10.0 messaging-namespace); no observation yet. (Structured evidence: oracle-conformance.test.ts)
Messaging.get app(): AppThe admin App this Messaging instance is bound to.
Upstream typings (firebase-admin 13.10.0 messaging); no observation yet. (Structured evidence: oracle-conformance.test.ts)
Messaging.send(message, dryRun?): Promise<string>Sends one message via FCM v1 and resolves with the resource name projects/<projectId>/messages/<numeric id>. dryRun=true returns the SAME shape (fake id), so callers cannot distinguish validation from acceptance by shape. Topic, condition, token, notification-only, data-only, and webpush-config sends are all accepted. Malformed sends fail server-side validation with HTTP 4xx google.rpc error envelopes carrying both a google.rpc.BadRequest (fieldViolations) and a google.firebase.fcm.v1.FcmError (errorCode); detail ordering is not contractual. The documented data-payload cap is 4096 bytes.
Messaging.sendEach(messages, dryRun?): Promise<BatchResponse>Sends an array of up to 500 messages, one RPC per message; resolves a BatchResponse whose responses are ordered to match the input. Total failure is signalled by a throw or an all-failure BatchResponse.
Upstream typings/JSDoc (firebase-admin 13.10.0 messaging); no observation yet. (Structured evidence: oracle-conformance.test.ts)
Messaging.sendEachForMulticast(message, dryRun?): Promise<BatchResponse>Fans a MulticastMessage (up to 500 tokens) out through sendEach.
Upstream typings/JSDoc (firebase-admin 13.10.0 messaging); no observation yet. (Structured evidence: oracle-conformance.test.ts)
Messaging.subscribeToTopic(tokenOrTokens, topic): Promise<MessagingTopicManagementResponse>Subscribes one or many registration tokens to a topic; resolves a MessagingTopicManagementResponse.
Upstream typings/JSDoc (firebase-admin 13.10.0 messaging); no observation yet. (Structured evidence: oracle-conformance.test.ts)
Messaging.unsubscribeFromTopic(tokenOrTokens, topic): Promise<MessagingTopicManagementResponse>Unsubscribes one or many registration tokens from a topic; resolves a MessagingTopicManagementResponse.
Upstream typings/JSDoc (firebase-admin 13.10.0 messaging); no observation yet. (Structured evidence: oracle-conformance.test.ts)
Messaging.enableLegacyHttpTransport(): voidForces HTTP/1.1 transport for sendEach / sendEachForMulticast; deprecated.
Upstream typings/JSDoc (firebase-admin 13.10.0 messaging, @deprecated); no observation yet. (Structured evidence: oracle-conformance.test.ts)

Message union + targets

type Message = TokenMessage | TopicMessage | ConditionMessageA send payload carrying exactly one of token, topic, or condition.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface BaseMessage { data?; notification?; android?; webpush?; apns?; fcmOptions? }Common message fields shared by every target variant. Production accepts a message carrying ONLY a notification block and, separately, ONLY a data block — neither is individually required.
oracle: messaging-send-notification-only-vs-data-only-accepted.json (both accepted). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface TokenMessage extends BaseMessage { token: string }A device-token target. A syntactically invalid token is rejected with HTTP 400 INVALID_ARGUMENT whose fieldViolations name message.token.
oracle: messaging-send-invalid-token-error-envelope.json (fieldViolations names message.token). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface TopicMessage extends BaseMessage { topic: string }A topic target. A well-formed topic send is accepted and returns the standard resource name (no subscribers required); a topic name containing characters outside the documented [a-zA-Z0-9-_.~%] set is rejected with an INVALID_ARGUMENT error envelope.
oracle: messaging-send-topic-accepted.json (accepted) + messaging-send-invalid-topic-name-error-envelope.json (bad name rejected). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface ConditionMessage extends BaseMessage { condition: string }A condition target. A well-formed condition of the form "'a' in topics && 'b' in topics" is accepted with the standard resource-name shape (no subscribers required); a malformed condition (dangling operator) is rejected with an error envelope.
oracle: messaging-send-condition-accepted.json (accepted) + messaging-send-invalid-condition-error-envelope.json (malformed rejected). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface MulticastMessage extends BaseMessage { tokens: string[] }A multicast target of up to 500 tokens, fanned out by sendEachForMulticast.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)

Payload / config option shapes

interface Notification { title?; body?; imageUrl? }Top-level, platform-independent notification block. Production accepts a notification-only message (no data block).
oracle: messaging-send-notification-only-vs-data-only-accepted.json. Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface FcmOptions { analyticsLabel? }Platform-independent FCM options (analyticsLabel).
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface WebpushConfig { headers?; data?; notification?; fcmOptions? }Webpush overrides. Production accepts a webpush config carrying headers.TTL and fcmOptions.link; a non-numeric headers.TTL is rejected with an error envelope.
oracle: messaging-send-webpush-config-accepted.json (accepted) + messaging-send-webpush-invalid-ttl-error-envelope.json (bad TTL rejected). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface WebpushFcmOptions { link? }Webpush FCM options (link, HTTPS required). Production accepts fcmOptions.link on a webpush send.
oracle: messaging-send-webpush-config-accepted.json (link accepted). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)
interface WebpushNotification { title?; actions?; badge?; body?; dir?; icon?; image?; renotify?; requireInteraction?; silent?; tag?; vibrate?; [key] }Web Notification API-shaped options, including an open-ended index signature.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface ApnsConfig { liveActivityToken?; headers?; payload?; fcmOptions? }APNs overrides.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface ApnsPayload { aps; [customData] }APNs payload wrapper carrying the required aps dictionary plus arbitrary custom keys.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface Aps { alert?; badge?; sound?; contentAvailable?; mutableContent?; category?; threadId?; [customData] }APNs aps dictionary; alert is a string or an ApsAlert, sound a string or a CriticalSound.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface ApsAlert { title?; subtitle?; body?; locKey?; locArgs?; ...; launchImage? }APNs alert object with title/subtitle/body and localization keys.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface CriticalSound { critical?; name; volume? }APNs critical sound — name required; volume in the range 0.0–1.0.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface ApnsFcmOptions { analyticsLabel?; imageUrl? }APNs FCM options (analyticsLabel, imageUrl).
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface AndroidConfig { collapseKey?; priority?; ttl?; restrictedPackageName?; data?; notification?; fcmOptions?; ... }Android overrides; ttl is in milliseconds and priority is high | normal.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface AndroidNotification { title?; body?; icon?; color?; sound?; tag?; imageUrl?; channelId?; priority?; visibility?; lightSettings?; ... }Android notification options, including localization keys, LED light settings, and delivery-proxy controls.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface LightSettings { color; lightOnDurationMillis; lightOffDurationMillis }Android LED light settings — all three fields required.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface AndroidFcmOptions { analyticsLabel? }Android FCM options (analyticsLabel).
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)

Legacy payload shapes

interface DataMessagePayload { [key]: string }Legacy data payload — up to 4KB; the keys from and google.* are reserved.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface NotificationMessagePayload { tag?; body?; icon?; badge?; color?; sound?; title?; ...; [key] }Legacy notification payload with localization keys, a clickAction, and arbitrary string keys.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface MessagingPayload { data?; notification? }Legacy combined payload — one or both of data / notification required.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface MessagingOptions { dryRun?; priority?; timeToLive?; collapseKey?; mutableContent?; contentAvailable?; restrictedPackageName?; [key] }Legacy send options; documented defaults are dryRun false, ttl 2419200s (4 weeks), priority high for notifications / normal for data.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)

Response shapes

interface MessagingTopicManagementResponse { failureCount; successCount; errors }Topic subscribe / unsubscribe result carrying per-index errors as FirebaseArrayIndexError[].
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface BatchResponse { responses; successCount; failureCount }Batch send result; responses is a SendResponse[].
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)
interface SendResponse { success; messageId?; error? }Per-message batch entry — on success messageId is set, on failure error is set.
Upstream typings (firebase-admin 13.10.0 messaging-api); no observation yet. (Structured evidence: oracle-conformance.test.ts)

Errors

class FirebaseMessagingError extends PrefixedFirebaseErrorThe exported admin messaging error type.
Upstream typings (firebase-admin 13.10.0 lib/messaging/index); no observation yet. (Structured evidence: oracle-conformance.test.ts)
class MessagingClientErrorCodeExported static { code, message } members (INVALID_ARGUMENT, INVALID_RECIPIENT, INVALID_PAYLOAD, … UNKNOWN_ERROR). The wire-level INVALID_ARGUMENT FcmError returned by malformed sends maps to MessagingClientErrorCode.INVALID_ARGUMENT.
oracle: messaging-send-no-target-error-envelope.json + messaging-send-invalid-token-error-envelope.json (both carry the INVALID_ARGUMENT FcmError). Replayed by the conformance suite. (Structured evidence: oracle-conformance.test.ts)

Behavior classes — quota, retry, offline delivery (send plane)

FCM send quota and rate-limit throttlingFCM enforces project-level send quotas and per-target rate limits (per-device and per-topic message rates) accounted on Google servers over rolling windows; exceeding them yields quota / rate-limit rejections. The admin error taxonomy names these as MessagingClientErrorCode.MESSAGE_RATE_EXCEEDED, DEVICE_MESSAGE_RATE_EXCEEDED, and TOPICS_MESSAGE_RATE_EXCEEDED (wire QUOTA_EXCEEDED / RESOURCE_EXHAUSTED, HTTP 429). Stated from FCM quota documentation; the sandbox has no global quota ledger to model.
Upstream documentation (FCM quotas and MessagingClientErrorCode rate-limit members, firebase-admin 13.10.0); no observation. This behavior is out of sandbox scope (see the row disposition).
Send retry and backoff on transient transport failuresThe admin SDK retries transient send-transport failures — connection errors and HTTP 5xx / SERVER_UNAVAILABLE / INTERNAL responses — with backoff before surfacing the error to the caller, so a caller sees a resolved send or a terminal error rather than every intermediate failure. enableLegacyHttpTransport switches sendEach / sendEachForMulticast from HTTP/2 to HTTP/1.1, changing multiplexing and the retry/transport path. Stated from the admin SDK and google-auth transport documentation; no committed observation replays the retry count or backoff schedule yet.
Upstream documentation (firebase-admin 13.10.0 send transport + google-auth retry defaults); no observation yet. Candidate probe: point the send transport at a fault-injecting stub that returns 503 then 200 and record the retry count and backoff intervals before the resolved send.
Offline store-and-forward deliveryTTL and collapse-key semantics · When a target device is offline FCM stores the message and delivers it on reconnect, up to its time-to-live (android.ttl, APNs expiration, webpush headers.TTL; the legacy default is 2419200 seconds / four weeks). A collapse key (android.collapseKey, collapseKey) marks a message as collapsible so a newer collapsible message replaces an older still-undelivered one, and only the last is delivered on reconnect; a message whose TTL elapses before reconnect is dropped rather than delivered. Stated from FCM message-lifecycle documentation; no committed observation replays store-and-forward, collapse, or TTL expiry yet.
Upstream documentation (FCM message lifecycle: TTL, collapse keys, store-and-forward); no observation yet. Candidate probe: send several collapsible messages to a token whose web client is offline, bring it online within TTL, and assert only the last collapsible message is delivered; separately, let a short-TTL message expire before reconnect and assert it is dropped.

Current gaps

Not implemented yet

Tracked behavior that is not implemented in the current contract.

Registration-token inactivity expiry (server-side staleness window)FCM treats a registration token that has gone unused for an extended period (Google documents roughly 270 days of app-instance inactivity) as stale and drops it from delivery targeting, independent of any explicit deleteToken. The expiry clock is Google server-side inactivity accounting, and the recommended mitigation is periodic token refresh. Stated from FCM best-practices documentation; the sandbox has no server-side inactivity clock to model.
Upstream documentation (FCM registration-token best practices); no observation. This time-based server-side behavior is out of sandbox scope (see the row disposition); the explicit-deletion path to a dead token is already tracked by messaging#3.
FCM send quota and rate-limit throttlingFCM enforces project-level send quotas and per-target rate limits (per-device and per-topic message rates) accounted on Google servers over rolling windows; exceeding them yields quota / rate-limit rejections. The admin error taxonomy names these as MessagingClientErrorCode.MESSAGE_RATE_EXCEEDED, DEVICE_MESSAGE_RATE_EXCEEDED, and TOPICS_MESSAGE_RATE_EXCEEDED (wire QUOTA_EXCEEDED / RESOURCE_EXHAUSTED, HTTP 429). Stated from FCM quota documentation; the sandbox has no global quota ledger to model.
Upstream documentation (FCM quotas and MessagingClientErrorCode rate-limit members, firebase-admin 13.10.0); no observation. This behavior is out of sandbox scope (see the row disposition).

Unverified

Tracked behavior whose available evidence does not yet establish the production result.

Multi-tab delivery routing (several window clients, one shared service worker)When several window clients (browser tabs) of one origin share a single service-worker registration, a foreground delivery reaches every visible tab through onMessage, and when no tab is visible the one shared service worker receives the message once through onBackgroundMessage (the worker is shared across tabs, not instantiated per tab). A notification click focuses a single existing client rather than opening a duplicate. Stated from the FCM web SDK and the Service Worker client model; no committed observation replays the multi-tab fan-out yet.
Upstream documentation (@firebase/messaging 0.12.26 + W3C Service Worker Clients model); no observation yet. Candidate probe: open two tabs on one registration and deliver with one tab visible, then with both hidden — assert onMessage fires once per visible tab and onBackgroundMessage fires exactly once on the shared worker.
Multi-device delivery (one app instance holding several tokens; per-token fan-out)A single user or app can hold several registration tokens at once — one per browser, profile, or device. A token-targeted send reaches exactly one token; a topic, condition, or multicast send fans out independently to every subscribed token, with no server-side de-duplication across a user’s devices, so each device receives its own copy. Stated from the FCM targeting model; no committed observation replays cross-device fan-out yet.
Upstream documentation (FCM targeting model); no observation yet. Candidate probe: mint two distinct tokens, subscribe both to one topic, send once, and assert each token receives the message independently.
Registration-token rotation and refresh over the app lifecycleA registration token is stable within a session (row messaging#2 pins that repeated getToken calls on one service-worker registration return the same token) but is not permanent across the app lifecycle: the SDK rotates it on events such as app reinstall, SDK upgrade, a restored backup, or a notification-permission change, and Google may retire a token server-side. The modular web SDK has no token-refresh callback, so an app observes a new token only by calling getToken again; a rotated-away old token eventually stops delivering and the send plane reports the dead token (the UNREGISTERED path pinned by messaging#3). This lifecycle rotation is deliberately outside the within-session stability observation. Stated from FCM token-management documentation; no committed observation replays rotation across lifecycle events yet.
Upstream documentation (FCM token-management guidance); the committed messaging-web-token-stability observation covers only within-session stability, not lifecycle rotation. Candidate probe (long-horizon): force a service-worker unregister/re-register or an app-instance reset, re-mint via getToken, and record whether the token changed and whether the prior token then reports UNREGISTERED on send.
Send retry and backoff on transient transport failuresThe admin SDK retries transient send-transport failures — connection errors and HTTP 5xx / SERVER_UNAVAILABLE / INTERNAL responses — with backoff before surfacing the error to the caller, so a caller sees a resolved send or a terminal error rather than every intermediate failure. enableLegacyHttpTransport switches sendEach / sendEachForMulticast from HTTP/2 to HTTP/1.1, changing multiplexing and the retry/transport path. Stated from the admin SDK and google-auth transport documentation; no committed observation replays the retry count or backoff schedule yet.
Upstream documentation (firebase-admin 13.10.0 send transport + google-auth retry defaults); no observation yet. Candidate probe: point the send transport at a fault-injecting stub that returns 503 then 200 and record the retry count and backoff intervals before the resolved send.
Offline store-and-forward deliveryWhen a target device is offline FCM stores the message and delivers it on reconnect, up to its time-to-live (android.ttl, APNs expiration, webpush headers.TTL; the legacy default is 2419200 seconds / four weeks). A collapse key (android.collapseKey, collapseKey) marks a message as collapsible so a newer collapsible message replaces an older still-undelivered one, and only the last is delivered on reconnect; a message whose TTL elapses before reconnect is dropped rather than delivered. Stated from FCM message-lifecycle documentation; no committed observation replays store-and-forward, collapse, or TTL expiry yet.
Upstream documentation (FCM message lifecycle: TTL, collapse keys, store-and-forward); no observation yet. Candidate probe: send several collapsible messages to a token whose web client is offline, bring it online within TTL, and assert only the last collapsible message is delivered; separately, let a short-TTL message expire before reconnect and assert it is dropped.