Push context – device push-token registry and (see send_to_user/3)
server-authoritative delivery of push notifications.
Devices register their FCM registration token or APNs device token against
the authenticated user; a user has many devices. Delivery routes per
token off the provider column ("fcm" | "apns"), falling back to the
zero-config Log provider when nothing is configured
(see docs/specs/push.md).
Usage
# Register a device (typically via POST /me/push-tokens)
{:ok, token} = Push.register_token(user_id, %{
"token" => "fcm-registration-token",
"platform" => "android",
"device_id" => "stable-device-key"
})
# List a user's devices
tokens = Push.list_tokens(user_id, page: 1, page_size: 25)
# Remove one (DELETE /me/push-tokens/:id)
{:ok, _} = Push.delete_token(user_id, token.id)Note: This is an SDK stub. Calling these functions will raise an error. The actual implementation runs on the GameServer.
Summary
Functions
Remove any token row by id (admin). Returns {:ok, %PushToken{}} or
{:error, :not_found}.
Count for list_all_tokens/2 (same filters).
Count a user's registered tokens (including disabled).
Remove a token row by id, scoped to user_id (the DELETE /me/push-tokens/:id
path). Returns {:ok, %PushToken{}} or {:error, :not_found}.
Soft-disable a token the provider reported dead. The row is kept — re-registration re-enables it — so a token bouncing between valid and invalid never loses its device association.
Admin listing across all users. Supported filters keys (atom or string):
:user_id, :platform, :provider, and :status ("live" | "disabled").
List a user's registered tokens, newest first. Includes disabled rows (they are the user's devices; clients can show them greyed out).
The user's live (non-disabled) tokens — the delivery fan-out set. Unpaginated
by design: bounded by max_push_tokens_per_user.
Bump last_used_at after a successful delivery.
Resolve the delivery provider for a token: its provider column's module
when that module's configured?/0 says it can deliver, else the zero-config
Log provider. force_log: true (PUSH_ADAPTER=log) short-circuits
everything to Log.
Register (or refresh) a device push token for user_id.
Queue a push message to all of user_id's live devices.
Queue a push message to every live device of user_ids.
Aggregate token counts for the admin stat card and runtime introspection:
%{total: n, live: n, disabled: n, by_platform: %{...}, by_provider: %{...}}
(platform/provider maps count live tokens only).
Remove a token row by its raw token value, scoped to user_id.
Whether the user has any live device. Cached (60s TTL + version bump on
register/remove/disable): Notifications asks this on every insert, and the
common no-device answer must not cost a query.
Types
Functions
@spec admin_delete_token(Ecto.UUID.t()) :: {:ok, GameServer.Push.PushToken.t()} | {:error, :not_found}
Remove any token row by id (admin). Returns {:ok, %PushToken{}} or
{:error, :not_found}.
@spec count_all_tokens(map()) :: non_neg_integer()
Count for list_all_tokens/2 (same filters).
@spec count_tokens(user_id()) :: non_neg_integer()
Count a user's registered tokens (including disabled).
@spec delete_token(user_id(), Ecto.UUID.t()) :: {:ok, GameServer.Push.PushToken.t()} | {:error, :not_found}
Remove a token row by id, scoped to user_id (the DELETE /me/push-tokens/:id
path). Returns {:ok, %PushToken{}} or {:error, :not_found}.
@spec disable_token(String.t()) :: :ok
Soft-disable a token the provider reported dead. The row is kept — re-registration re-enables it — so a token bouncing between valid and invalid never loses its device association.
@spec list_all_tokens( map(), keyword() ) :: [GameServer.Push.PushToken.t()]
Admin listing across all users. Supported filters keys (atom or string):
:user_id, :platform, :provider, and :status ("live" | "disabled").
Supports pagination via :page and :page_size options; preloads :user
so the admin UI can show names, not UUIDs.
@spec list_tokens( user_id(), keyword() ) :: [GameServer.Push.PushToken.t()]
List a user's registered tokens, newest first. Includes disabled rows (they are the user's devices; clients can show them greyed out).
Supports pagination via :page and :page_size options.
@spec live_tokens(user_id()) :: [GameServer.Push.PushToken.t()]
The user's live (non-disabled) tokens — the delivery fan-out set. Unpaginated
by design: bounded by max_push_tokens_per_user.
@spec mark_token_used(String.t()) :: :ok
Bump last_used_at after a successful delivery.
@spec provider_for(GameServer.Push.PushToken.t()) :: module()
Resolve the delivery provider for a token: its provider column's module
when that module's configured?/0 says it can deliver, else the zero-config
Log provider. force_log: true (PUSH_ADAPTER=log) short-circuits
everything to Log.
@spec register_token(user_id(), map()) :: {:ok, GameServer.Push.PushToken.t()} | {:error, :too_many_tokens | Ecto.Changeset.t()}
Register (or refresh) a device push token for user_id.
Upsert semantics, serialized under the :push_tokens advisory lock:
- a row with the same
tokenalready exists → it is claimed for this user/device (a device that logged into another account must not keep receiving the old account's pushes) and re-enabled; - else a row with the same
(user_id, device_id)exists → its token is rotated in place and the row re-enabled; - else a new row is inserted, subject to
max_push_tokens_per_user(counting live tokens only).
provider defaults from the platform when omitted: "ios" → "apns",
anything else → "fcm".
Returns {:ok, %PushToken{}}, {:error, :too_many_tokens}, or
{:error, changeset}.
Queue a push message to all of user_id's live devices.
Server-authoritative: exposed to plugins through the SDK and to admins — never as a public client endpoint. Best-effort by design: no live devices means no jobs, and delivery failures never propagate back to the caller.
Returns :ok or {:error, errors} when the message fails validation
(see GameServer.Push.Message.new/1).
Queue a push message to every live device of user_ids.
Small audiences enqueue delivery jobs inline; past 100
recipients the expansion itself becomes a FanoutWorker job (chunked,
restart-safe, deduped against identical double-broadcasts).
@spec token_stats() :: map()
Aggregate token counts for the admin stat card and runtime introspection:
%{total: n, live: n, disabled: n, by_platform: %{...}, by_provider: %{...}}
(platform/provider maps count live tokens only).
@spec unregister_token(user_id(), String.t()) :: {:ok, GameServer.Push.PushToken.t()} | {:error, :not_found}
Remove a token row by its raw token value, scoped to user_id.
Returns {:ok, %PushToken{}} or {:error, :not_found}.
Whether the user has any live device. Cached (60s TTL + version bump on
register/remove/disable): Notifications asks this on every insert, and the
common no-device answer must not cost a query.