Server-side push notifications for Mob apps.
A thin server library that wraps:
- APNs HTTP/2 (iOS) — token-based auth with a
.p8key - FCM HTTP v1 (Android) — OAuth2 via Google service account
Token storage and fan-out are intentionally out of scope — bring your own persistence.
Setup
Add to your mix.exs:
{:mob_push, "~> 0.1"}Then run mix mob_push.install for interactive credential setup, or configure
manually in config/runtime.exs:
# iOS
config :mob_push, :apns,
key_id: "XXXXXXXXXX", # 10-char Key ID from Apple Developer portal
team_id: "XXXXXXXXXX", # 10-char Team ID from Membership Details
bundle_id: "com.example.myapp",
key_file: "/run/secrets/AuthKey_XXXXXXXXXX.p8",
env: :production # :sandbox | :production
# Android
config :mob_push, :fcm,
project_id: "my-firebase-project",
service_account_key: "/run/secrets/fcm_service_account.json"See the README for the full credential walkthrough.
Receiving device tokens in the app
In your Mob.Screen, request permission and register for push:
def on_mount(socket) do
socket = Mob.Permissions.request(socket, :notifications)
{:ok, socket}
end
def handle_info({:permission, :notifications, :granted}, socket) do
{:noreply, Mob.Notify.register_push(socket)}
end
def handle_info({:push_token, platform, token}, socket) do
MyApp.PushTokens.upsert(socket.assigns.user_id, token, platform)
{:noreply, socket}
endSending notifications
MobPush.send(token, :ios, %{
title: "New message",
body: "Alice: Hey, are you free tonight?",
subtitle: "in #general",
badge: 3,
sound: "default",
data: %{screen: "chat", thread_id: "42"}
})
MobPush.send(token, :android, %{
title: "New message",
body: "Alice: Hey, are you free tonight?",
data: %{screen: "chat", thread_id: "42"},
android: %{
"notification" => %{
"icon" => "ic_notification",
"color" => "#FF6200EE",
"channel_id" => "messages"
}
}
})Handling received notifications in the app
All three delivery scenarios (foreground, background tap, killed-then-tapped)
deliver the same {:notification, notif} message to your screen:
def handle_info({:notification, notif}, socket) do
# notif has string keys: "title", "body", "data"
case get_in(notif, ["data", "screen"]) do
"chat" -> {:noreply, Mob.Socket.push_screen(socket, MyApp.ChatScreen)}
_ -> {:noreply, socket}
end
endPayload options
| Key | Platforms | Type | Description |
|---|---|---|---|
:title | both | string | Notification title (required) |
:body | both | string | Notification body text (required) |
:subtitle | iOS | string | Second line under the title |
:data | both | map | Arbitrary key-value pairs delivered to the app |
:badge | iOS | integer | Badge count on the app icon (0 to clear) |
:sound | iOS | string | "default" or a filename bundled in the app |
:content_available | iOS | boolean | Silent push — wakes app in background, no alert |
:android | Android | map | Raw FCM AndroidConfig for appearance customization |
Return values
:ok— accepted by APNs / FCM{:error, :device_token_expired}— stale APNs token; delete it{:error, :device_token_not_found}— FCM doesn't know this token; delete it{:error, :auth_failed}— credentials rejected; check your config{:error, {:apns_error, reason}}— APNs rejected with a reason string{:error, {:fcm_error, status, message}}— FCM HTTP error{:error, :missing_apns_key_config}—:key_file/:key_pemnot configured{:error, {:apns_key_file_unreadable, path, reason}}—.p8file not readable{:error, :missing_fcm_service_account_config}— service account not configured
Summary
Functions
@spec send(device_token :: String.t(), platform :: :ios | :android, payload :: map()) :: :ok | {:error, term()}
Send a push notification to a device.
platform is :ios or :android.
payload must include :title and :body. Optional keys: :data,
:badge, :sound, :content_available, :android.
Like send/3 but raises on error.