PushX (PushX v0.5.0)

Copy Markdown View Source

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: 5000

Direct 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

message()

@type message() :: String.t() | map() | PushX.Message.t()

option()

@type option() :: PushX.APNS.option() | PushX.FCM.option()

provider()

@type provider() :: :apns | :fcm

token()

@type token() :: String.t()

Functions

check_rate_limit(provider)

@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.

message()

@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")

message(title, body)

@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")

push(provider, device_token, message, opts \\ [])

@spec push(provider(), token(), message(), [option()]) ::
  {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}

Sends a push notification to a device.

Arguments

  • provider - :apns for iOS or :fcm for Android
  • device_token - The device's push token
  • message - A string, map, or PushX.Message struct
  • opts - Provider-specific options

Options

APNS Options

  • :topic - Bundle ID (required for APNS)
  • :mode - :prod or :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"}}

push!(provider, device_token, message, opts \\ [])

@spec push!(provider(), token(), message(), [option()]) :: :ok | :error

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

push_batch(provider, device_tokens, message, opts \\ [])

@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 - :apns for iOS or :fcm for Android
  • device_tokens - List of device tokens
  • message - A string, map, or PushX.Message struct
  • opts - 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()}.

push_batch!(provider, device_tokens, message, opts \\ [])

@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!")

valid_token?(provider, token)

@spec valid_token?(provider(), token()) :: boolean()

Returns true if the token format is valid.

Delegates to PushX.Token.valid?/2.

validate_token(provider, token)

@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")