Modern push notifications for Elixir.
PushX provides a simple, unified API for sending push notifications to iOS (APNS) and Android (FCM) devices using HTTP/2 connections.
Features
- HTTP/2 connections via Finch (Mint-based)
- JWT authentication for APNS with automatic caching
- OAuth2 authentication for FCM via Goth
- Unified API with direct provider access
- Structured response handling
- Batch sending with configurable concurrency
- Token validation
- Client-side rate limiting
Quick Start
# Send to iOS
PushX.push(:apns, device_token, "Hello World", topic: "com.example.app")
# Send to Android
PushX.push(:fcm, device_token, "Hello World")
# With title and body
PushX.push(:apns, token, %{title: "New Message", body: "You have a notification"}, topic: "...")
# Batch send to multiple devices
results = PushX.push_batch(:fcm, tokens, "Hello Everyone!")Configuration
config :pushx,
# APNS (Apple)
apns_key_id: "ABC123DEFG",
apns_team_id: "TEAM123456",
apns_private_key: {:file, "priv/keys/AuthKey.p8"},
apns_mode: :prod,
# FCM (Firebase)
fcm_project_id: "my-project-id",
fcm_credentials: {:file, "priv/keys/firebase.json"},
# Batch sending
batch_concurrency: 50,
# Rate limiting (optional)
rate_limit_enabled: false,
rate_limit_apns: 5000,
rate_limit_fcm: 5000Direct Provider Access
For more control, use the provider modules directly:
# APNS
PushX.APNS.send(token, payload, topic: "com.app.bundle", mode: :sandbox)
# FCM
PushX.FCM.send(token, payload, data: %{"key" => "value"})
Summary
Functions
Checks if a request can be made within rate limits.
Creates a new message using the builder pattern.
Creates a new message with title and body.
Sends a push notification to a device.
Sends a push notification and returns only :ok or :error.
Sends a push notification to multiple devices concurrently.
Sends a push notification to multiple devices and returns success count.
Returns true if the token format is valid.
Validates a device token format.
Types
@type message() :: String.t() | map() | PushX.Message.t()
@type option() :: PushX.APNS.option() | PushX.FCM.option()
@type provider() :: :apns | :fcm
@type token() :: String.t()
Functions
@spec check_rate_limit(provider()) :: :ok | {:error, :rate_limited}
Checks if a request can be made within rate limits.
Delegates to PushX.RateLimiter.check/1.
Only applies when rate limiting is enabled in config.
@spec message() :: PushX.Message.t()
Creates a new message using the builder pattern.
Alias for PushX.Message.new/0.
Examples
message = PushX.message()
|> PushX.Message.title("Hello")
|> PushX.Message.body("World")
@spec message(String.t(), String.t()) :: PushX.Message.t()
Creates a new message with title and body.
Alias for PushX.Message.new/2.
Examples
message = PushX.message("Hello", "World")
@spec push(provider(), token(), message(), [option()]) :: {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
Sends a push notification to a device.
Arguments
provider-:apnsfor iOS or:fcmfor Androiddevice_token- The device's push tokenmessage- A string, map, orPushX.Messagestructopts- Provider-specific options
Options
APNS Options
:topic- Bundle ID (required for APNS):mode-:prodor:sandbox(default: from config):push_type- "alert", "background", "voip" (default: "alert"):priority- 5 or 10 (default: 10)
FCM Options
:project_id- Firebase project ID (default: from config):data- Custom data payload map
Examples
# Simple string message
PushX.push(:apns, token, "Hello!", topic: "com.example.app")
# Map with title and body
PushX.push(:fcm, token, %{title: "Alert", body: "Something happened"})
# Using Message struct
message = PushX.Message.new()
|> PushX.Message.title("Order Update")
|> PushX.Message.body("Your order has been shipped!")
|> PushX.Message.badge(1)
PushX.push(:apns, token, message, topic: "com.example.app")Returns
{:ok, %PushX.Response{provider: :apns, status: :sent, id: "..."}}
{:error, %PushX.Response{provider: :apns, status: :invalid_token, reason: "BadDeviceToken"}}
Sends a push notification and returns only :ok or :error.
Useful when you don't need the full response details.
Examples
case PushX.push!(:apns, token, "Hello", topic: "com.app") do
:ok -> Logger.info("Sent!")
:error -> Logger.warning("Failed")
end
@spec push_batch(provider(), [token()], message(), [option()]) :: [ {token(), {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}} ]
Sends a push notification to multiple devices concurrently.
Uses Task.async_stream for parallel sending with configurable concurrency.
Each result contains the token and the response.
Arguments
provider-:apnsfor iOS or:fcmfor Androiddevice_tokens- List of device tokensmessage- A string, map, orPushX.Messagestructopts- Provider-specific options plus::concurrency- Max concurrent requests (default: 50):timeout- Timeout per request in ms (default: 30_000):validate_tokens- Validate tokens before sending (default: false)
Examples
# Send to multiple iOS devices
results = PushX.push_batch(:apns, tokens, "Hello!", topic: "com.example.app")
# Process results
Enum.each(results, fn
{token, {:ok, response}} ->
Logger.info("Sent to #{token}: #{response.id}")
{token, {:error, response}} ->
if PushX.Response.should_remove_token?(response) do
MyApp.Tokens.delete(token)
end
end)
# With higher concurrency
PushX.push_batch(:fcm, tokens, "Alert!", concurrency: 100)Returns
A list of {token, result} tuples where result is {:ok, Response.t()} or {:error, Response.t()}.
@spec push_batch!(provider(), [token()], message(), [option()]) :: %{ success: non_neg_integer(), failure: non_neg_integer(), total: non_neg_integer() }
Sends a push notification to multiple devices and returns success count.
Simplified version of push_batch/4 that returns aggregate results.
Returns
A map with :success, :failure, and :total counts.
Examples
%{success: 95, failure: 5, total: 100} =
PushX.push_batch!(:fcm, tokens, "Hello!")
Returns true if the token format is valid.
Delegates to PushX.Token.valid?/2.
@spec validate_token(provider(), token()) :: :ok | {:error, PushX.Token.validation_error()}
Validates a device token format.
Delegates to PushX.Token.validate/2.
Examples
:ok = PushX.validate_token(:apns, valid_token)
{:error, :invalid_length} = PushX.validate_token(:apns, "too-short")