FaroEx (FaroEx v0.1.0)

Copy Markdown View Source

Send push notifications through a Faro server.

Build a client for one app, then send. Payloads are plain string-keyed maps in the wire shape, and FaroEx.Audience supplies helpers that keep tag audiences readable.

client = FaroEx.client(base_url: "https://push.example.com", api_key: System.fetch_env!("FARO_KEY"))

FaroEx.send(client,
  title: "Flash sale",
  body: "50% off for the next 3 hours",
  audience: %{
    "include" => [
      FaroEx.Audience.users(["42", "43"]),
      FaroEx.Audience.tag("plan", "pro")
    ]
  }
)
#=> {:ok, %{notification: %{...}, delivery: %{"sent" => 12, ...}}}

Failures are returned, never raised

{:error, %FaroEx.Error{}} carries the HTTP status and the server's own error code. A transport failure arrives as status: nil, code: "transport_error".

No retries, by design

Faro has no idempotency key on its send endpoint, so a blind retry after a timeout pushes the same notification to real phones twice. Nothing here retries — configure retries in your own code only if you can accept a duplicate.

Summary

Functions

Builds a client for one Faro app.

Lists the deliveries of one notification.

Reads a notification back, with the deliveries frozen for it.

Sends a notification, returning what was accepted and what has been delivered so far.

Functions

client(opts)

@spec client(keyword()) :: FaroEx.Client.t()

Builds a client for one Faro app.

Options

  • :base_url (required) — origin of the Faro server, e.g. "https://push.example.com".
  • :api_key (required) — an app API key, "<appId>.<secret>". Mint one with the notifications:read and notifications:write scopes; this library does nothing else.
  • :connect_timeout — milliseconds, default 5000.
  • :receive_timeout — milliseconds, default 120000. A send blocks for the server's inline drain, which is 10–20 seconds for a ~1000-user audience, so this is deliberately generous.

The app id is read out of the token, so a key and an app id can never disagree. Raises ArgumentError for a token that is not an app API key — an operator token or a public client key (yk_…) fails here, at boot, rather than as a 403 at 3am.

deliveries(client, notification_id, opts \\ [])

@spec deliveries(FaroEx.Client.t(), String.t(), keyword()) ::
  {:ok, [map()]} | {:error, FaroEx.Error.t()}

Lists the deliveries of one notification.

Options

  • :status — "pending", "sending", "sent", "failed", "invalid" or "skipped".
  • :limit — 1 to 1000, default 200 server-side.

get_notification(client, notification_id)

@spec get_notification(FaroEx.Client.t(), String.t()) ::
  {:ok, %{notification: map() | nil, deliveries: [map()]}}
  | {:error, FaroEx.Error.t()}

Reads a notification back, with the deliveries frozen for it.

This is how you learn the real outcome: a send returns a snapshot, and the worker keeps finishing retries and delayed sends in the background. deliveries is the list the API returns (up to 200 rows); returns {:error, %FaroEx.Error{code: "not_found"}} for an unknown id.

send(client, opts)

@spec send(FaroEx.Client.t(), keyword()) ::
  {:ok, %{notification: map() | nil, delivery: map() | nil}}
  | {:error, FaroEx.Error.t()}

Sends a notification, returning what was accepted and what has been delivered so far.

Options

  • :title, :subtitle, :body, :data, :priority, :ttl, :url, :icon, :image, :launch_url, :channel_id, :collapse_id, :send_after_seconds, :audience — the notification content as the API defines it. :data is an arbitrary map handed to the client app; :audience is an audience map (see FaroEx.Audience). Options you leave out are not sent.

Returns

{:ok, %{notification: %{...}, delivery: %{"sent" => 12, ...}}}

Both maps are the server's JSON, decoded with string keys. The counts are a snapshot: retries of provider hops finish in the background, so a "pending" count above zero means there is more to come — poll get_notification/2 for the final tally.

A send accepted with zero deliveries (a user with no device) is {:ok, _}: the request landed, and an empty audience is not an error.

This call blocks for the server's inline drain — roughly 10–20 seconds for a ~1000-user audience — so run it under your own supervisor (Task.Supervisor.async_nolink/3) rather than inside a bare request cycle.