Portal Community

Node Configuration

{
  "nodeType": "SendPushNotification",
  "name": "pushApprovalAlert",
  "config": {
    "credentialId": 22,
    "provider": "fcm",
    "deviceToken": "$output.fetchUserDevice.fcmToken",
    "title": "Approval Required",
    "body": "Invoice $output.createInvoice.invoiceNumber needs your sign-off",
    "data": {
      "executionId": "$context.executionId",
      "invoiceId": "$output.createInvoice.invoiceId",
      "deepLink": "acme://approvals/$output.createInvoice.invoiceId"
    },
    "badge": 1,
    "sound": "default",
    "priority": "high",
    "ttlSeconds": 86400
  }
}

Configuration Fields

FieldTypeRequiredDescription
credentialIdintYesFCM service account key JSON or APNs p8 key, stored via ICredentialResolver
providerfcm | apnsYesPush provider. Determines adapter used.
deviceTokenstring / exprYesFCM registration token or APNs device token for the target device.
titlestring / exprYesNotification title shown in device notification tray.
bodystring / exprYesNotification body text.
dataobjectNoCustom key-value data payload delivered to the app (not shown in tray). Values are evaluated expressions.
badgeint / exprNoiOS badge count. Ignored by FCM on Android.
soundstringNoNotification sound. Use "default" or a custom sound file name bundled in the app.
priorityhigh | normalNoFCM message priority. high wakes the device immediately.
ttlSecondsintNoTime-to-live in seconds. If device is offline longer than TTL, message is dropped.

FCM vs APNs

AspectFCM (Firebase Cloud Messaging)APNs (Apple Push)
PlatformAndroid (also iOS via FCM bridge)iOS, macOS, watchOS
Credential typeService account JSON keyp8 key file + Team ID + Key ID + Bundle ID
Token formatFCM registration token (long string)64-char hex device token
Priorityhigh / normal10 (immediate) / 5 (power-save)
Data-only messageOmit notification key — data onlySet content-available: 1

PushChannelAdapter — FCM

public class PushChannelAdapter : IMessagingChannelAdapter
{
    public string ChannelType => "Push";

    public async Task<MessagingResult> SendAsync(
        MessagingMessage message,
        MessagingConfig config,
        CancellationToken ct)
    {
        if (config.Provider == "apns")
            return await SendViaApns(message, config, ct);

        // FCM path
        var serviceAccountJson = await _credentials.GetPasswordAsync(config.CredentialId, ct);
        var credential = GoogleCredential
            .FromJson(serviceAccountJson)
            .CreateScoped("https://www.googleapis.com/auth/firebase.messaging");

        var fcmMessage = new Message
        {
            Token = message.To,
            Notification = new Notification { Title = message.Title, Body = message.Body },
            Data = message.Data?.ToDictionary(k => k.Key, k => k.Value?.ToString() ?? ""),
            Android = new AndroidConfig
            {
                Priority = config.Priority == "high"
                    ? Priority.High : Priority.Normal,
                Ttl = TimeSpan.FromSeconds(config.TtlSeconds ?? 86400)
            }
        };

        var app = FirebaseApp.Create(new AppOptions { Credential = credential });
        var messageId = await FirebaseMessaging.DefaultInstance.SendAsync(fcmMessage, ct);

        return new MessagingResult
        {
            MessageId = messageId,
            Channel = "push",
            Recipient = message.To,
            SentAt = DateTimeOffset.UtcNow,
            Status = "sent"
        };
    }
}
Data-only notifications: To send a silent background push (no UI notification) for in-app processing, omit title and body and include only a data payload. On FCM Android, this is a "data message". On APNs, combine with content-available: 1.
Token staleness: Device tokens expire when a user reinstalls the app or resets their device. Store tokens in your entity system and listen for FCM's NotRegistered error to clean up stale tokens automatically.