Firebase Cloud Messaging (FCM) client.
Sends push notifications to Android devices and web browsers using the FCM v1 API with OAuth2 authentication via Goth.
Configuration
Add to your config:
config :pushx,
fcm_project_id: "my-project-id",
fcm_credentials: {:file, "priv/keys/firebase-service-account.json"}Usage
# Simple notification
PushX.FCM.send(device_token, %{
"notification" => %{
"title" => "Hello",
"body" => "World"
}
})
# Using Message struct
message = PushX.Message.new("Hello", "World")
PushX.FCM.send(device_token, message)
# Notification with custom data
PushX.FCM.send(device_token, %{
"notification" => %{"title" => "Alert", "body" => "Something happened"},
"data" => %{"event_id" => "1"}
})
# Data-only (silent) message — no visible notification
PushX.FCM.send_data(device_token, %{action: "sync", id: 123})
# Data-only via structured payload
PushX.FCM.send(device_token, %{"data" => %{"action" => "sync"}})Topics and Conditions
Any send accepts a topic or condition where the token goes (see
target/0), and subscribe/3 / unsubscribe/3 manage memberships:
PushX.FCM.subscribe(tokens, "news")
PushX.FCM.send({:topic, "news"}, message)
PushX.FCM.send({:condition, "'news' in topics && 'sports' in topics"}, message)Dry Runs
# FCM validates token and payload without delivering anything
PushX.FCM.send(device_token, message, validate_only: true)Web Push (Chrome, Firefox, Edge)
FCM supports web push using the same API. Web tokens come from the browser's
Firebase Messaging SDK (firebase.messaging().getToken()).
# Web push with click action
PushX.FCM.send(web_token, payload,
webpush: %{
"fcm_options" => %{"link" => "https://example.com/page"}
}
)
# Using web notification helper
payload = PushX.FCM.web_notification("Title", "Body", "https://example.com")
PushX.FCM.send(web_token, payload)
Summary
Types
Where an FCM message goes: a device registration token, a topic
({:topic, "news"} — the name only, without the /topics/ prefix), or a
condition ({:condition, "'news' in topics && 'sports' in topics"}).
Topics and conditions fan out server-side, so PushX.Response carries no
per-device information for them and should_remove_token?/1 is never true.
Per-token outcome of subscribe/3 / unsubscribe/3.
Functions
Creates a simple notification payload.
Sends a push notification to an Android device with automatic retry.
Sends notifications to multiple devices concurrently.
Sends a data-only message (no visible notification) with automatic retry.
Sends a data-only message without retry.
Sends a push notification without retry.
Sends a web push notification with automatic retry.
Subscribes device tokens to an FCM topic ({:topic, name} targets in
send/3 then reach them).
Unsubscribes device tokens from an FCM topic (iid/v1:batchRemove). See subscribe/3.
Creates a web push notification payload with click action.
Types
@type payload() :: map() | PushX.Message.t()
Where an FCM message goes: a device registration token, a topic
({:topic, "news"} — the name only, without the /topics/ prefix), or a
condition ({:condition, "'news' in topics && 'sports' in topics"}).
Topics and conditions fan out server-side, so PushX.Response carries no
per-device information for them and should_remove_token?/1 is never true.
@type token() :: String.t()
Per-token outcome of subscribe/3 / unsubscribe/3.
Functions
Creates a simple notification payload.
Examples
iex> PushX.FCM.notification("Hello", "World")
%{"title" => "Hello", "body" => "World"}
@spec send(target(), payload(), [option()]) :: {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
Sends a push notification to an Android device with automatic retry.
Uses exponential backoff for transient failures following Google's best practices. Permanent failures (bad token, invalid argument) are not retried.
Options
:project_id- Firebase project ID (default: from config):data- Custom data payload map:android- Android-specific configuration:apns- APNS configuration (for iOS via FCM):webpush- Web push configuration:validate_only-trueasks FCM to validate the message (token registration, payload) without delivering it — a dry run. A successful{:ok, %Response{status: :sent}}then means "FCM would have accepted this"; errors (:unregistered,:invalid_request, ...) are the real ones. Useful for verifying a stored token before a campaign.:retry-:blocking(default) or:none(single attempt); seePushX.push/4
Returns
{:ok, %PushX.Response{}}on success{:error, %PushX.Response{}}on failure
@spec send_batch(Enumerable.t(), payload(), [option()]) :: [ {target(), {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}} ]
Sends notifications to multiple devices concurrently.
Options
All standard options plus:
:concurrency- Max concurrent requests (default: 50):timeout- Timeout per request in ms (default:PushX.Config.batch_timeout_ms/0, sized to the worst-case retry backoff):validate_tokens- Validate token format before sending (default: false). Invalid tokens get{:error, %Response{status: :invalid_token}}without hitting the network.
Each task runs the full blocking retry cycle; if you pass an explicit
:timeout, make sure it exceeds the worst-case retry backoff or disable
retries — see PushX.push_batch/4 for details.
Returns
A list of {token, result} tuples.
@spec send_data(target(), map(), [option()]) :: {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
Sends a data-only message (no visible notification) with automatic retry.
@spec send_data_once(target(), map(), [option()]) :: {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
Sends a data-only message without retry.
@spec send_once(target(), payload(), [option()]) :: {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
Sends a push notification without retry.
Use this when you want to handle retries yourself or for testing.
@spec send_web(token(), String.t(), String.t(), String.t(), keyword()) :: {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
Sends a web push notification with automatic retry.
Convenience function that combines web_notification/4 with send/3.
Examples
PushX.FCM.send_web(web_token, "Hello", "World", "https://example.com")
# With options
PushX.FCM.send_web(web_token, "Alert", "Check this out",
"https://example.com/page",
icon: "https://example.com/icon.png"
)
@spec subscribe([token()], String.t(), keyword()) :: {:ok, [topic_result()]} | {:error, PushX.Response.t()}
Subscribes device tokens to an FCM topic ({:topic, name} targets in
send/3 then reach them).
Uses the Instance ID API (iid/v1:batchAdd) with the same OAuth token as
sends. Google accepts at most 1 000 tokens per request; larger lists are
chunked and sent sequentially. Returns one outcome per token, in input
order — :ok, or {:error, code} with Google's per-token error string
("NOT_FOUND" for an unregistered token, "INVALID_ARGUMENT",
"TOO_MANY_TOPICS", ...). A request-level failure (OAuth, network, non-2xx)
is returned as {:error, %PushX.Response{}} for the whole batch.
Options: :project_id is not needed (topics are per project of the OAuth
credentials); :retry and :receive_timeout/:pool_timeout apply as for
send/3. In delivery: :test mode nothing is contacted and every token
is reported :ok.
Examples
{:ok, results} = PushX.FCM.subscribe(tokens, "news")
for {token, {:error, "NOT_FOUND"}} <- results, do: MyApp.Tokens.delete(token)
@spec unsubscribe([token()], String.t(), keyword()) :: {:ok, [topic_result()]} | {:error, PushX.Response.t()}
Unsubscribes device tokens from an FCM topic (iid/v1:batchRemove). See subscribe/3.
Creates a web push notification payload with click action.
This helper creates a notification optimized for web browsers (Chrome, Firefox, Edge).
The link option specifies the URL to open when the notification is clicked.
Arguments
title- Notification titlebody- Notification bodylink- URL to open when clickedopts- Optional keyword list::icon- Icon URL for the notification:image- Large image URL:badge- Badge icon URL (small monochrome icon):tag- Tag for notification grouping:renotify- Whether to alert again for same tag (default: false):require_interaction- Keep notification until user interacts (default: false)
Examples
# Simple web notification
PushX.FCM.web_notification("New Message", "You have a new message", "https://example.com/messages")
# With icon and badge
PushX.FCM.web_notification("Sale!", "50% off today",
"https://shop.com",
icon: "https://shop.com/icon.png",
badge: "https://shop.com/badge.png"
)