# `PushX`
[🔗](https://github.com/cignosystems/pushx/blob/v0.11.0/lib/push_x.ex#L1)

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: "...")

    # Notification with custom data (FCM)
    PushX.push(:fcm, token, %{
      "notification" => %{"title" => "Alert", "body" => "Event triggered"},
      "data" => %{"event_id" => "123"}
    })

    # Data-only (silent) message (FCM)
    PushX.push_data(:fcm, token, %{action: "sync", id: 123})

    # 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"})

# `instance_name`

```elixir
@type instance_name() :: atom()
```

# `message`

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

# `option`

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

# `provider`

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

# `token`

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

# `check_rate_limit`

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

# `health_check`

```elixir
@spec health_check() :: %{apns: map(), fcm: map()}
```

Returns health status for each configured provider.

Includes configuration status and circuit breaker state.

## Examples

    PushX.health_check()
    #=> %{
    #=>   apns: %{configured: true, circuit: :closed},
    #=>   fcm: %{configured: true, circuit: :closed}
    #=> }

# `message`

```elixir
@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`

```elixir
@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`

```elixir
@spec push(provider() | instance_name(), 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!`

```elixir
@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`

```elixir
@spec push_batch(provider() | instance_name(), [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). When
    `true`, invalid tokens get `{:error, %Response{status: :invalid_token}}`
    without ever leaving the local process — the result list always matches
    the input length.

## 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!`

```elixir
@spec push_batch!(provider() | instance_name(), [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!")

# `push_data`

```elixir
@spec push_data(provider() | instance_name(), token(), map(), [option()]) ::
  {:ok, PushX.Response.t()} | {:error, PushX.Response.t()}
```

Sends a data-only (silent) push notification to a device.

The message contains only a `data` payload with no visible notification.
Useful for triggering background syncs or delivering structured data.

## Arguments

  * `provider` - `:fcm` or a named instance atom
  * `device_token` - The device's push token
  * `data` - A map of key-value data (values will be stringified)
  * `opts` - Provider-specific options

## Examples

    # Via default FCM config
    PushX.push_data(:fcm, token, %{action: "sync", id: 123})

    # Via named instance
    PushX.push_data(:my_fcm, token, %{action: "sync", id: 123})

# `reconnect`

```elixir
@spec reconnect() :: :ok | {:error, term()}
```

Restarts the Finch HTTP pool, forcing fresh connections.

Call this when connections become stale (e.g., after persistent
`too_many_concurrent_requests` or `request_timeout` errors). On cloud
infrastructure like Fly.io, idle HTTP/2 connections can be silently
dropped, and Finch cannot detect these zombie connections. Restarting
the pool forces new TCP/TLS handshakes.

This is called automatically by the retry logic on connection errors.
You can also call it manually if needed.

## Examples

    PushX.reconnect()
    #=> :ok

# `valid_token?`

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

Returns true if the token format is valid.

Delegates to `PushX.Token.valid?/2`.

# `validate_token`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
