Pigeon - iOS and Android push notifications for Elixir

Pigeon 2.0.0-rc.0 Released

It’s been a long time coming, but it’s here! A lot of significant updates and new features, including the FCM v1 API with web-push support and better runtime configuration of push workers.

This will serve as an unofficial migration guide until I’ve written one. Feedback is welcome!

Hexdocs available here.

Using the New Worker Setup

Pigeon 1.x uses default expected atom names for workers and supervised all of them independent from your application. This has been done away with for more of an Ecto-style setup in your supervision tree. The following example is for APNS, but it’s a similar strategy for all of your other push workers.

  1. Create an APNS dispatcher.
# lib/apns.ex
defmodule YourApp.APNS do
  use Pigeon.Dispatcher, otp_app: :your_app
end
  1. (Optional) Add configuration to your config.exs. Note the :adapter key, which tells Pigeon what kind of worker you are setting up.
# config.exs

config :your_app, YourApp.APNS,
  adapter: Pigeon.APNS,
  cert: File.read!("cert.pem"),
  key: File.read!("key_unencrypted.pem"),
  mode: :dev
  1. Start your dispatcher on application boot.
defmodule YourApp.Application do
  @moduledoc false

  use Application

  @doc false
  def start(_type, _args) do
    children = [
      YourApp.APNS
    ]
    opts = [strategy: :one_for_one, name: YourApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

If you skipped step two, include your configuration.

defmodule YourApp.Application do
  @moduledoc false

  use Application

  @doc false
  def start(_type, _args) do
    children = [
      {YourApp.APNS, apns_opts()}
    ]
    opts = [strategy: :one_for_one, name: YourApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  defp apns_opts do
    [
      adapter: Pigeon.APNS,
      cert: File.read!("cert.pem"),
      key: File.read!("key_unencrypted.pem"),
      mode: :dev
    ]
  end
end
  1. Create a notification.
n = Pigeon.APNS.Notification.new("your message", "your device token", "your push topic")
  1. Send the notification. Note: you use the module name! Not Pigeon.APNS.
YourApp.APNS.push(n)

Using this strategy should make it far more flexible for worker setup, including things like loading dynamic push configurations from a database. See Pigeon.Dispatcher for more instructions.

FCM v1.0 API

All legacy Pigeon.FCM functionality has been renamed to Pigeon.LegacyFCM. You will need to do a project-wide rename, or else update to the new configuration and notification structure.

# Before
Pigeon.FCM.Notification.new("reg ID", %{"body" => "test message"})

# After
Pigeon.LegacyFCM.Notification.new("reg ID", %{"body" => "test message"})

The new Pigeon.FCM.Notification exposes all of the v1 notification attributes you might expect to use, but makes no guarantees about structure and typing, apart from the top-level keys. This gets you maximum flexibility to use the API how you need, including things like custom APNS options or web push.

Notification targets replace the typical RegID string. You now pass a tuple for what you want, such as {:token, "regid"}, {:topic, "yourtopic"}, or {:condition, "yourcondition"}.

Pigeon.FCM.Notification.new({:token, "reg ID"})
%Pigeon.FCM.Notification{
  data: nil,
  notification: nil,
  target: {:token, "reg ID"}
}

Pigeon.FCM.Notification.new({:topic, "example"})
%Pigeon.FCM.Notification{
  data: nil,
  notification: nil,
  target: {:topic, "example"}
}

Notification moduledocs here. You also might want to reference the Firebase v1 Message API as well.

Custom Adapters

Probably the coolest feature of this update is the new Pigeon.Adapter behaviour, which all Pigeon workers implement. What does this mean long term? Anybody can write a push adapter for Pigeon, whether that’s a third party push service, SMS, webhooks, or anything else. Expect better documentation on this soon, but in the meantime if you’re interested in writing an adapter, reach out to me and I can help!

Here is an example of the Pigeon.Sandbox adapter, which is useful for tests and local development. It will mark any push as :success and return it.

defmodule Pigeon.Sandbox do
  import Pigeon.Tasks, only: [process_on_response: 1]

  @behaviour Pigeon.Adapter

  @impl true
  def init(opts \ []) do
    {:ok, opts}
  end

  @impl true
  def handle_info(_msg, state) do
    {:noreply, state}
  end

  @impl true
  def handle_push(%{response: nil} = notification, on_response, _state) do
    process_on_response(on_response, %{notification | response: :success})
    {:noreply, state}
  end

  def handle_push(notification, on_response, state) do
    process_on_response(on_response, notification)
    {:noreply, state}
  end
end

Worker Pooling By Default

Now that APNS allows multiple persistent connections, worker pooling has been enabled by default. The default value is 5 for all services, but this can be overridden on a per-dispatcher basis or globally.

# Just the FCM worker
config :your_app, YourApp.FCM,
  pool_size: 10

# Globally
config :pigeon, :default_pool_size, 10

Next Steps

This release removes a lot of the cruft from design choices that were made in the early days of Pigeon. It is highly encouraged to adopt the new FCM v1 API. Many users of Pigeon jumped through hoops to get dynamic runtime workers started. Feedback is welcome on the new style-- hopefully it’s far more pleasant!

So what’s next feature-wise? Telemetry metrics. Expect it in a 2.1 release!

11 Likes