Class FcmTransport

java.lang.Object
com.iizix.server.push.engine.transport.FcmTransport
All Implemented Interfaces:
IPushTransport

public final class FcmTransport extends Object implements IPushTransport
The Firebase Cloud Messaging transport, HTTP v1: POST https://fcm.googleapis.com/v1/projects/{project}/messages:send with an OAuth 2.0 bearer token minted from the application's service account.

The subscription is the registration token the Cordova client obtained from the FirebaseX plugin. Both Android and iOS devices are reached here: with the plugin in FCM mode the iOS token is an FCM registration token and FCM relays through APNs (ruling on the iOS transport, 2026-09-06). An PushTransport.APNS row, a raw APNs token registered by the legacy client branch, is not this transport's.

Message mapping, PUSH-WIRE-CONTRACT section 6.5: notification.title, body and image from the message; data from options.data, every value a string as FCM requires, plus the actions as JSON text and every member that has no native FCM field (navigate, badge, renotify, requireInteraction, lang, dir), so the device can rebuild the object the Service Worker receives; android.ttl and apns-expiration from the TTL; android.priority and apns-priority from the urgency; android.notification.tag and the apns-collapse-id header from options.tag; android.notification.icon from options.icon, as given; android.notification.event_time from options.timestamp as RFC 3339; android.notification.vibrate_timings from options.vibrate as durations, "0.2s", after a leading "0s" because Android's first element is the delay before the vibrator starts where the Notification API's first element is the first vibration; android.notification.notification_priority PRIORITY_MIN from options.silent when true. A member the message does not carry produces no field, and no sound field is written for either platform.

data.iizi (PUSH-WIRE-CONTRACT section 6.2) is rebuilt here as one JSON string: the members the server put in it - the message ID, the application, the Topic, the send time - and, on this leg only, notification, the whole content object without its data. FirebaseX hands the app's JavaScript nothing but data, so the members mapped to native fields above would otherwise never reach it; the Web Push leg carries no such copy, since its Service Worker decrypts the whole object (ruled 2026-09-23, CC-PUSH-P3 RR-11, landed by CC-PUSH-FCM-DATA-IIZI). FCM refuses a payload over MAX_PAYLOAD; payloadSize(PushMessage) measures it so that push refuses up front, and send(SubscriptionRecord, PushMessage, long, DeliveryUrgency) refuses it again before any request, the backstop.

The target seam (CC-PUSH-CORDOVA-FIX C1e, ruling R-4 A of 2026-09-26): buildMessage(String, PushMessage, long, DeliveryUrgency) addresses a FcmTransport.Target, a registration token or an FCM Topic, and can ask FCM to validate the request without delivering it (validate_only, a member of the request beside message). send(SubscriptionRecord, PushMessage, long, DeliveryUrgency) always addresses the subscription's token and never validates only. The Topic and the validation serve the P4-13 measurement of MAX_PAYLOAD against a Topic with no subscribers, no device needed (P4-13 rulings P13-2 and P13-4); no server path sends to an FCM Topic (PUSH-WIRE-CONTRACT section 2.4).

The subscribe check (CC-PUSHDEV-VALIDATE-ONLY, D-2 (c) of CC-FIX-PUSHDEV-PROJECT, ruled 2026-09-28): checkToken(String, String) posts a validate_only body to a registration token with the application's credentials, so that a device whose token names no Firebase project is told apart at subscribe time - FCM answers 200 for a token of the application's project and SENDER_ID_MISMATCH for another project's, and delivers nothing either way. The verdict, a FcmTransport.TokenCheck, is the value a later caller reuses; the definitive ones are kept per application, project and token, since a token's project never changes. A subscribe path whose token another project issued asks again with *'s credentials, checkToken("*",...), and a match records the row under *'s project (B-10b, RR-21 C).

The credentials are chosen by the row (PUSH-CLUSTER-DESIGN 3.3, B-10b, AppPushConfigs.signer(SubscriptionRecord)): a row made under the server's own * project - a device inside iiziRun Developer subscribed for an application (RR-21 C, ruled PC-9) - is posted to *'s project with *'s service account; a row made under a project this server holds for neither is DeliveryOutcome.KEY_MISMATCH, nothing is posted, and the row is kept.

Status mapping: 200 delivered; 404 with UNREGISTERED, or 400 whose error names the registration token, endpoint stale; 429, 500, 503 transient with Retry-After; 401 and 403 the credentials, permanent and the cached token dropped - except a 403 whose error code is SENDER_ID_MISMATCH, the registration token's fault and not the credentials': the token was issued by another Firebase project than the one the row is signed through - since B-10b only a row that records no project, stored before B-10a or by a device that named none and FCM could not check - so the row is refused with a detail that says so and the OAuth token FCM accepted is kept (ruled 2026-09-26, CC-PUSH-ROADMAP row 4); other 4xx permanent.

Author:
Christopher Mindus
  • Field Details

    • MAX_PAYLOAD

      public static final int MAX_PAYLOAD
      The largest message payload FCM accepts, in bytes: the notification and the data together (Firebase: "maximum payload for both message types is 4096 bytes"). How FCM counts is not published; payloadSize(JSONObject) counts the compact JSON text of the two objects, which is what FCM is given.
      See Also:
  • Constructor Details

    • FcmTransport

      public FcmTransport(HttpClient client, AppPushConfigs configs)
      Constructor.
      Parameters:
      client - The started Jetty client.
      configs - The application configurations.
  • Method Details

    • getKind

      public PushTransport getKind()
      Description copied from interface: IPushTransport
      Gets the transport kind.
      Specified by:
      getKind in interface IPushTransport
      Returns:
      The kind.
    • getState

      public TransportState getState(String appId)
      Description copied from interface: IPushTransport
      Gets whether this transport can send for an application.
      Specified by:
      getState in interface IPushTransport
      Parameters:
      appId - The application ID, * for iiziRun.
      Returns:
      The state, never null.
    • send

      public TransportResult send(SubscriptionRecord subscription, PushMessage message, long ttlMillis, DeliveryUrgency urgency)
      Description copied from interface: IPushTransport
      Sends a notification to one subscription. Blocks the calling (virtual) thread until the push service has answered or the request timed out.
      Specified by:
      send in interface IPushTransport
      Parameters:
      subscription - The subscription.
      message - The notification.
      ttlMillis - The effective time-to-live in milliseconds, already defaulted and clamped by the engine; converted to seconds at the header only.
      urgency - The effective urgency.
      Returns:
      The result, never null; a failure is an outcome, never an exception.
    • checkToken

      public FcmTransport.TokenCheck checkToken(String appId, String registrationToken)
      Checks the Firebase project of a registration token at subscribe time (CC-PUSHDEV-VALIDATE-ONLY): posts buildMessage(Target,PushMessage,long,DeliveryUrgency,boolean) to the token with validate_only and the application's credentials, so FCM validates the request and delivers nothing. 200 is FcmTransport.ProjectVerdict.MATCHES; SENDER_ID_MISMATCH is FcmTransport.ProjectVerdict.ANOTHER, with the detail send(SubscriptionRecord, PushMessage, long, DeliveryUrgency) would report; anything else is FcmTransport.ProjectVerdict.UNKNOWN - an application without a configured transport, no OAuth token, FCM unreachable or slower than CHECK_TIMEOUT_SECONDS, a stale or malformed token, a credential refusal (which drops the cached OAuth token, as a send does) or a transient error.

      The two definitive verdicts are kept per application, project and token, at most VERDICTS, so that a device registering one token for many rows costs one request; an unknown one is asked again next time. Blocking: call it off any worker or connection thread.

      Parameters:
      appId - The application.
      registrationToken - The device's FCM token.
      Returns:
      The outcome, never null.
    • buildMessage

      public static JSONObject buildMessage(String registrationToken, PushMessage message, long ttlMillis, DeliveryUrgency urgency)
      Parameters:
      registrationToken - The device token.
      message - The notification.
      ttlMillis - The TTL.
      urgency - The urgency.
      Returns:
      The {"message":{...}} object.
    • buildMessage

      public static JSONObject buildMessage(FcmTransport.Target target, PushMessage message, long ttlMillis, DeliveryUrgency urgency, boolean validateOnly)
      Builds the FCM v1 request body for a target, a registration token or a Topic, delivering or validating only.

      The data values, ruled 2026-09-22 (CC-PUSH-FCM-MAP ruling 3): every value is the text FCM requires and the receiver cannot tell the type back. A number is its decimal text (7 to "7"), a boolean "true" or "false", an object or an array its JSON text, a JSON null the empty string, a string itself. The members carried in data (DATA_MEMBERS and actions) follow the same rule and, ruled the same day (ruling 6), overwrite an application data key of the same name; PUSH-WIRE-CONTRACT section 6.2 lists those names as reserved. data.iizi is the JSON text of the server's object with notification added: the content object without its data, carried on this leg only (CC-PUSH-FCM-DATA-IIZI).

      validate_only (P4-13, ruling P13-2): FCM's messages:send takes it as a member of the request beside message, never inside it, and validates the message without delivering it. It is written only when asked, so a body that does not ask is the body send(SubscriptionRecord, PushMessage, long, DeliveryUrgency) posts.

      Parameters:
      target - The registration token or the Topic.
      message - The notification.
      ttlMillis - The TTL.
      urgency - The urgency.
      validateOnly - true to have FCM validate the request and deliver nothing.
      Returns:
      The {"message":{...}} object, with "validate_only":true beside message when asked.
    • payloadSize

      public static int payloadSize(PushMessage message)
      Measures the FCM payload a notification has on this leg, data.iizi.notification included, for a refusal before anything is sent (PUSH-WIRE-CONTRACT section 6.3). The token, the TTL and the urgency do not count and are stood in for.
      Parameters:
      message - The notification.
      Returns:
      The size in bytes.
    • dispose

      public void dispose()
      Description copied from interface: IPushTransport
      Releases the transport's resources.
      Specified by:
      dispose in interface IPushTransport