Skip to main content

Push notification API

Centrifugo excels in delivering real-time in-app messages to online users. Sometimes though you need a way to engage offline users to come back to your app, or to trigger some update in the app while it's running in the background. That's where push notifications may be used. Push notifications are delivered over a battery-efficient, platform-dependent transport.

With Centrifugo PRO push notifications may be delivered to all popular application platforms:

  • Android devices
  • iOS devices
  • Web browsers which support Web Push API — desktop and mobile, including installed PWAs on Android and iOS (Chrome, Firefox, Edge, Safari; see this matrix). With native Web Push this is the easiest path to push — no Firebase, no native app.

Centrifugo PRO provides an API to manage user device tokens and device topic subscriptions, and an API to send push notifications to registered devices and groups of devices (subscribed to a topic). The API also supports timezone-aware push notifications, push localizations, templating, and per-user device push rate limiting. You can also use your own device token storage and use Centrifugo PRO as a high-performance way to send push notifications to supported providers.

Push

To deliver push notifications to devices Centrifugo PRO integrates with the following providers:

FCM, HMS, APNs, and Web Push handle the frontend and transport aspects of notification delivery. Device token storage, management, and efficient push notification broadcasting are managed by Centrifugo PRO. Tokens are stored in a PostgreSQL database. To facilitate efficient push notification broadcasting to devices, Centrifugo PRO includes worker queues based on Redis streams (and also provides an option to use a PostgreSQL-based queue).

Integration with FCM means that you can use existing Firebase messaging SDKs to extract a push notification token for a device on different platforms (iOS, Android, Flutter, web browser) and set up push notification listeners. The same applies to HMS and APNs - just use existing native SDKs and best practices on the frontend. Only a couple of additional steps are required to integrate the frontend with Centrifugo PRO device token and device topic storage. After doing that you will be able to send push notifications to a single device, or to a group of devices subscribed to a topic. For example, with a simple Centrifugo API call like this:

curl -X POST http://localhost:8000/api/send_push_notification \
-H "Authorization: apikey <KEY>" \
-d @- <<'EOF'

{
"recipient": {
"filter": {
"topics": ["test"]
}
},
"notification": {
"fcm": {
"message": {
"notification": {"title": "Hello", "body": "How are you?"}
}
}
}
}
EOF

In addition, Centrifugo PRO includes a helpful web UI for inspecting registered devices and sending push notifications:

Motivation and design choices

Centrifugo PRO tries to be practical with its Push Notification API, let's look at its design choices and implementation properties.

Storage for tokens

To start delivering push notifications in the application, developers usually need to integrate with providers such as FCM, HMS, and APNs. This integration typically requires the storage of device tokens in the application database and the implementation of sending push messages to provider push services.

Centrifugo PRO simplifies the process by providing a backend for device token storage, following best practices in token management. It reacts to errors and periodically removes inactive devices/tokens to keep the stored set healthy, based on provider recommendations.

Efficient queuing

Additionally, Centrifugo PRO provides an efficient, scalable queuing mechanism for sending push notifications. Developers can send notifications from the app backend to Centrifugo API with minimal latency and let Centrifugo process sending to FCM, HMS, APNs concurrently using built-in workers. In our tests, we achieved several millions pushes per minute.

Centrifugo PRO also supports a delayed push notifications feature – to queue a push for later delivery, so for example you can send a notification based on user time zone and let Centrifugo PRO send it when needed.

Unified secure topics

FCM and HMS have a built-in way of sending notification to large groups of devices over topics mechanism (the same for HMS). Topics are great since you can create segments and groups of devices and target specific ones with your notifications.

One problem with native FCM or HMS topics though is that clients can subscribe to any topic from the frontend side without any permission check. In today's world this is usually not desired. So Centrifugo PRO re-implements FCM and HMS topics by introducing an additional API to manage device subscriptions to topics.

tip

In some cases you may have real-time channels and device subscription topics with matching names – to send messages to both online and offline users. Though it's up to you.

Centrifugo PRO device topic subscriptions also add a way to introduce the missing topic semantics for APNs.

Centrifugo PRO additionally lets you keep a per-user list of topics (user_topic_update). This is the list you manage; Centrifugo copies it onto a device when the device registers — registering (or re-registering) a device for a user subscribes that device to the user's current topics (and only those, plus any topics you pass in the call). This solves one of the pains with FCM – if two different users share one device it's hard to unsubscribe the device from a large number of topics on logout: registering the device for the new user (or removing it) switches the whole set in one call, with no need for your backend to track topics one by one.

Changing a user's topics with user_topic_update takes effect on that user's already-registered devices immediately — Centrifugo applies the change to those devices as part of the call. Two exceptions catch up at the next device_register instead: a brand-new device that hasn't registered yet, and the global "" binding (which applies to every user and would otherwise touch every device at once). See Device lifecycle and best practices for the exact rules, including that device_update's user_update changes the user field without re-copying topics. You can also skip the per-user list entirely and pass the full topic list in each device_register call.

Push personalization

Centrifugo PRO provides several ways to make push notifications individual and take care about better user experience with notifications. This includes:

All these features may be used on individual request basis.

Send in each provider's own format

Unlike solutions that merge every provider's API into one combined format, Centrifugo PRO passes your notification straight through to each provider. You write the notification in the format each provider already defines, so there's no extra format to learn in between.

It's also possible to send notifications into native FCM, HMS topics or send to raw FCM, HMS, APNs tokens using Centrifugo PRO's push API, allowing them to combine native provider primitives with those added by Centrifugo (i.e., sending to a list of device IDs or to a list of topics).

Builtin analytics

Furthermore, Centrifugo PRO offers the ability to inspect sent push notifications using ClickHouse analytics. Providers may also offer their own analytics, such as FCM, which provides insight into push notification delivery. Centrifugo PRO also offers a way to analyze push notification delivery and interaction using the update_push_status API.

Steps to integrate

  1. Add provider SDK on the frontend side, follow provider instructions for your platform to obtain a push token for a device. For example, for FCM see instructions for iOS, Android, Flutter, Web Browser). The same for HMS or APNs – frontend part should be handled by their native SDKs.
  2. Call the Centrifugo PRO backend API with the obtained token. From the application backend, call the Centrifugo device_register API to register the device in Centrifugo PRO storage. Optionally provide a list of topics to subscribe the device to.
  3. Centrifugo returns a registered device object. Pass the generated device ID to the frontend and save it on the frontend together with a token received from FCM.
  4. Call the Centrifugo send_push_notification API whenever it's time to deliver a push notification.

At any moment you can inspect the device storage by calling the device_list API.

Once a user logs out from the app, remove the device with the device_remove API, or re-register it with an empty user to keep the device but drop the user's topics. See Device lifecycle and best practices below for the exact rules (and why device_update is not the way to switch a device's user when you rely on user-bound topics).

Device lifecycle and best practices

Getting the device ID flow right is what keeps your device storage clean (no duplicates, no orphaned tokens). Device IDs are generated by Centrifugo — they embed a timestamp and the provider, and a client cannot choose an arbitrary ID (registering with a wrongly-formatted id is rejected). The robust pattern:

Who calls what (the device_register API is server-side — it needs your API key, so your backend calls it; never the frontend directly):

 [frontend]     get a push token from FCM / APNs / Web Push
│ also read the device_id you saved earlier, if any

[frontend] send the token (+ saved device_id, if any) to your backend


[your backend] call Centrifugo device_register (authenticated, API key)
│ • no device_id sent → Centrifugo creates a new device
│ • device_id sent → Centrifugo updates that device

[Centrifugo] stores the device, returns its device_id


[your backend] send the device_id back to the frontend


[frontend] save the device_id on the device

↻ Repeat all of this on every app start, and whenever the push token
changes. Because you send the saved device_id, the SAME device is
updated — so you never create duplicates or leave a dead token behind.

On logout:
[your backend] device_remove { id } → the device (and its topics) is deleted

Centrifugo also deletes a device on its own when the push provider reports
the token is dead (app uninstalled, notifications revoked, …).
device_register writes the device's whole state, not just the fields you change

Every call replaces the device record. provider, platform and token are required (empty values are rejected). Anything you leave out of user, timezone, locale or topics is reset to empty, and the device's topic list is rebuilt from the topics you pass plus the current user's bound topics. So always send the complete device state you want — and include the saved id so the existing device is updated instead of a duplicate being created.

This is intentional: declaring the full state (especially the owner) on every registration is what keeps shared devices safe — there's no way to accidentally leave a device attached to a previous user. To change one field without re-sending the token, use device_update (metadata) or device_topic_update (topics) — those are the partial-update methods; device_register is the full-state one.

1. First registration — omit id. Call device_register with provider, token, platform (and optionally user, topics, timezone, locale) and no id. Centrifugo creates the device and returns its id. Persist that id on the client together with the push token.

2. Re-registration — pass the stored id with the full device state. On app start, and especially when the provider rotates the push token, call device_register again with the stored id plus the complete state (provider, token, platform, user, and timezone/locale/topics if you use them — see the full-replace note above). This updates the same device. The behavior to understand:

  • Re-register with the stored id (token same or refreshed) → the existing device is updated. No duplicate. ✅ Recommended.
  • Re-register without id, token unchanged → Centrifugo recognizes the token (provider + token is unique) and returns the same device. Also fine.
  • Re-register without id, token changed → Centrifugo can't match the old device and creates a new one; the old token sticks around until its next push fails and is removed automatically. Passing the stored id avoids this temporary duplicate.

If the client lost its stored id (fresh install, cleared storage), just omit id — Centrifugo recognizes the token and returns the existing device, as long as the token is still valid.

3. Topics: prefer user-bound, and know that device_register rebuilds the device's set. A device gets topics from two sources, and each device_register rebuilds the device's topic set as the topics argument the topics currently bound to the device's user:

  • User-bound topics (recommended, convenient). Keep a topic list per user with user_topic_update. Then on every (re-)register you pass only the user — Centrifugo copies that user's topics onto the device for you. You never resend the topic list. This is usually all you need: subscribe a user to topics once, and all their devices pick them up. (Changes apply to the user's already-registered devices immediately; a device that hasn't registered yet picks them up when it does.)
  • Device-specific topics. The topics argument (and device_topic_update) attach topics to one device regardless of user. Because register rebuilds the set, these are dropped if you re-register without them — so either pass the full device-specific list on every device_register, or manage them via device_topic_update and don't re-register without including them. Most apps don't need this; reach for user-bound topics first.

4. Logout / user switch. To switch a device to a different user (and its topics), re-register the device with the new user — this re-syncs topics in one call. To log out: either device_remove (deletes the device and its topics) or re-register with an empty user (keeps the device, drops the user's topics). Note: device_update's user_update changes only the user field — it does not re-sync user-bound topics, so don't use it to switch users if you rely on user-bound topics; combine it with an explicit topics_update, or use device_register.

5. Automatic cleanup. When a provider reports a token/subscription is no longer valid (FCM UNREGISTERED, APNs 410, Web Push 404/410), Centrifugo removes that device automatically — this alone keeps the table healthy and is always on. You may additionally set max_inactive_device_interval to drop abandoned installs — devices whose app hasn't re-registered within the interval (updated_at reflects registration/update, not sends, so it measures app activity, not delivery). Use it for engagement apps that register on each app open; leave it unset for notification-centric apps (rarely opened, but you still want to reach them) and rely on the automatic dead-token cleanup instead.

Using Centrifugo-issued device IDs end to end also lets you correlate delivery/interaction analytics via update_push_status.

Notes on scale
  • Sending to a filter goes through matching devices page by page, so sending to large groups of users or topics stays fast even with many devices.
  • user_topic_update applies the change to the user's current devices in one transaction, so its cost grows with that user's device count (normally a handful). The global "" binding is the exception — it applies to every user, so it's left for each device's next registration rather than touching every device at once.
  • Each device_register rebuilds that device's topic list, so its cost grows with the number of topics on the device — keep per-device topic counts (and the number of topics every user gets via the empty-user "" list) reasonable if devices register often.
  • If two first-time registrations for the same token arrive at the same moment, one may get a conflict error; retrying it succeeds, because the retry finds and reuses the device the first one created.

Configuration

In Centrifugo PRO you can configure one push provider or use all of them – this choice is up to you.

Enabling push notifications

To enable push notifications, set push_notifications.enabled to true and specify which providers to use in push_notifications.enabled_providers list.

FCM

As mentioned above, Centrifugo uses PostgreSQL for token storage. To enable push notifications make sure database section defined in the configuration and fcm is in the push_notifications.enabled_providers list. Centrifugo PRO uses Redis Streams (default) or PostgreSQL for queuing push notification requests. Finally, to integrate with FCM a path to the credentials file must be provided (see how to create one in this instruction). So the full configuration to start sending push notifications over FCM may look like this:

config.json
{
"database": {
"enabled": true,
"postgresql": {
"dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"
}
},
"push_notifications": {
"enabled": true,
"queue": {
"redis": {
"address": "localhost:6379"
}
},
"enabled_providers": [
"fcm"
],
"fcm": {
"credentials_file": "/path/to/service/account/credentials.json"
}
}
}
tip

Actually, PostgreSQL database configuration is optional here – you can use push notifications API without it. In this case you will be able to send notifications to FCM, HMS, APNs raw tokens, FCM and HMS native topics and conditions. I.e. using Centrifugo as an efficient way to send push notifications (for example if you already keep tokens in your database). But sending to device ids and topics, and token/topic management APIs won't be available for usage.

HMS

{
"database": {
"enabled": true,
"postgresql": {
"dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"
}
},
"push_notifications": {
"enabled": true,
"queue": {
"redis": {
"address": "localhost:6379"
}
},
"enabled_providers": [
"hms"
],
"hms": {
"app_id": "<your_app_id>",
"app_secret": "<your_app_secret>"
}
}
}
tip

See example how to get app id and app secret here.

APNs

{
"database": {
"enabled": true,
"postgresql": {
"dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"
}
},
"push_notifications": {
"enabled": true,
"queue": {
"redis": {
"address": "localhost:6379"
}
},
"enabled_providers": [
"apns"
],
"apns": {
"endpoint": "development",
"bundle_id": "com.example.your_app",
"auth_type": "token",
"token_key_file": "/path/to/auth/key/file.p8",
"token_key_id": "<your_key_id>",
"token_team_id": "your_team_id"
}
}
}

Instead of token_key_file, you can provide the key content inline using token_key_pem:

{
"push_notifications": {
"apns": {
"token_key_pem": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
}
}
}

We also support auth over p12 certificates (set auth_type to "cert") with the following options:

  • push_notifications.apns.cert_p12_file - path to .p12 certificate file
  • push_notifications.apns.cert_p12_b64 - base64-encoded .p12 certificate content
  • push_notifications.apns.cert_p12_password - password for .p12 certificate

Web Push (VAPID)

Native Web Push delivers notifications directly to browsers using the standard Web Push protocol with VAPID — no Firebase required. Payloads are end-to-end encrypted to the browser's subscription keys per Message Encryption for Web Push (RFC 8291) using the aes128gcm encrypted content-encoding (RFC 8188), so the push service never sees the message content.

This is the easiest way to add push notifications to a web app — desktop and mobile. You generate a VAPID key pair once (a single command), set three config values, and add a service worker plus a pushManager.subscribe call on the frontend — no Firebase project, no Apple push certificates, no native app, no app store. One implementation covers desktop browsers (Chrome, Edge, Firefox, Safari) and mobile — Android browsers (Chrome, Firefox, …) and, on iOS/iPadOS 16.4+, web apps added to the Home Screen (installed PWAs). So with a single VAPID setup you reach browsers across every major OS.

Mobile specifics

On Android, Web Push works in the browser directly. On iOS/iPadOS it works for web apps the user has added to the Home Screen (an installed PWA) on Safari 16.4+ — it does not work in a regular Safari tab. Either way it's the same VAPID setup on your side; no per-platform code.

First generate a VAPID key pair (for example with npx web-push generate-vapid-keys, or the helper in our Web Push example). Then configure:

config.json
{
"database": {
"enabled": true,
"postgresql": {
"dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"
}
},
"push_notifications": {
"enabled": true,
"queue": {
"redis": {
"address": "localhost:6379"
}
},
"enabled_providers": [
"webpush"
],
"webpush": {
"vapid_public_key": "<your_vapid_public_key>",
"vapid_private_key": "<your_vapid_private_key>",
"subject": "mailto:you@example.com"
}
}
}

On the frontend, use the same vapid_public_key as the applicationServerKey when calling pushManager.subscribe. This yields a PushSubscription object that looks like this:

{
"endpoint": "https://fcm.googleapis.com/fcm/send/abc...",
"keys": {
"p256dh": "BN...",
"auth": "k9..."
}
}

To register the device, serialize this entire PushSubscription object to a JSON string and pass it as the token field of device_register. The token is a plain string field (see the device_register API), so this works identically over the HTTP and gRPC APIs and from any language — Centrifugo parses the string server-side. Do not pass the subscription as a nested object; it must be a string.

Typically you don't call device_register from the browser (it requires the API key), but from your backend, which receives the PushSubscription from the frontend and forwards it. Here is how to build the token in different languages:

import json
import requests

# subscription is the PushSubscription received from the browser (a dict).
payload = {
"provider": "webpush",
"platform": "web",
"user": "42",
# token is a STRING: the whole PushSubscription serialized as JSON.
"token": json.dumps(subscription),
}

requests.post(
"http://localhost:8000/api/device_register",
json=payload,
headers={"Authorization": "apikey <CENTRIFUGO_API_KEY>"},
)

The examples above use the HTTP API, where the JSON library escapes the token string for you when serializing the outer request. Over the gRPC API it's even simpler: token is a protobuf string field, so you assign the serialized subscription to it directly and the framing handles the rest — no escaping involved. Either way you serialize the subscription exactly once and never hand-escape it.

tip

FCM can also deliver to browsers (via its webpush message config), but that requires Firebase. Native Web Push is the FCM-free path and the only way to reach Safari without Apple push certificates. Pick one path per browser device.

note

Each browser uses a different push service endpoint (Chrome → fcm.googleapis.com, Firefox → Mozilla autopush, Safari → web.push.apple.com), but Centrifugo speaks the standard Web Push protocol to all of them, so no per-browser configuration is needed. Web Push has no native topics/conditions — use Centrifugo device topics for grouping. Safari additionally requires the web app to be installed (added to Dock / Home Screen) before push works.

Token lifecycle and cleanup

The subscription endpoint is stored as the device token (its stable identity, like an FCM/APNs token); the encryption keys are stored alongside it. Registration validates the subscription per RFC 8291 (P-256 p256dh point, 16-byte auth) and rejects invalid ones. When a push service reports a subscription is gone (404/410), Centrifugo removes that device automatically. When a browser re-subscribes (new endpoint) the app should call device_register again with the new subscription; the obsolete one is cleaned up on its next failed send. As with other providers, the always-on dead-token cleanup (404/410) keeps the table healthy; the optional max_inactive_device_interval drops abandoned installs by registration recency, so enable it only for apps that re-register on each open (see the cleanup note under Device lifecycle).

Web Push endpoint SSRF protection

A Web Push endpoint is a URL that originates in the end user's browser and is later fetched by Centrifugo when sending a notification — the same class of outbound-request-to-a-user-supplied-URL as webhook delivery. Without controls, a malicious user could register an endpoint pointing at internal infrastructure (e.g. 169.254.169.254, localhost, a private service) and have Centrifugo issue requests there. Centrifugo applies the standard SSRF defenses, most of which are always on and not configurable:

  • https only — non-https endpoints are rejected (RFC 8030 requires TLS anyway).
  • No IP-literal hosts — the endpoint host must be a domain name; literal IPs (any form) are rejected, since no real push service uses one.
  • Private address block — connections are refused if the endpoint's host resolves to a loopback, private (RFC 1918 / IPv6 ULA), link-local (including the cloud-metadata address) or unspecified address. This check runs at connect time on the resolved IP, so it is not bypassable by DNS rebinding, and it has no opt-out.
  • No redirects — a 3xx from the push service is not followed (it could otherwise smuggle the request to a different host).

On top of that, Centrifugo restricts the origin of the endpoint to an allowlist. By default this is a built-in list of the mainstream browser push services, which covers every browser in practice:

https://*.googleapis.com                # Chrome / Chromium browsers (FCM: fcm.googleapis.com)
https://*.push.apple.com # Safari, iOS / iPadOS
https://*.notify.windows.com # Microsoft Edge / Windows (WNS)
https://*.push.services.mozilla.com # Firefox

You control this list with two options (glob patterns, same syntax as client.allowed_origins):

  • allowed_endpoint_originsreplaces the built-in list. Set it to pin delivery to a specific set of origins (or to ["*"] to allow any origin and rely only on the always-on checks above).
  • extra_allowed_endpoint_originsadded on top of the effective list (the built-in defaults, or your allowed_endpoint_origins if set). Use this to allow an extra origin — e.g. a self-hosted push service — without restating the defaults.
config.json
{
"push_notifications": {
"webpush": {
"extra_allowed_endpoint_origins": ["https://push.example.com"]
}
}
}
Self-hosted push services

Because the private-address block has no opt-out and IP-literal hosts are rejected, a self-hosted push service (e.g. self-hosted Mozilla autopush) must be reachable over https at a publicly-routable address via a domain name, and its origin must be allowed (via extra_allowed_endpoint_origins or allowed_endpoint_origins). Push services on private/internal IPs are not supported.

For defense in depth, you can additionally restrict Centrifugo's outbound network egress at the infrastructure layer (firewall, Kubernetes NetworkPolicy, or an egress proxy).

Use PostgreSQL as queue

Centrifugo PRO utilizes Redis Streams as the default queue engine for push notifications. However, it also offers the option to employ PostgreSQL for queuing. Set push_notifications.queue.type to "postgresql":

config.json
{
"database": {
"postgresql": {
"dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres"
},
"enabled": true
},
"push_notifications": {
"enabled": true,
"queue": {
"type": "postgresql",
"postgresql": {
"reuse_from_database": true
}
}
}
}
tip

Queue based on Redis streams is generally more efficient, so if you start with PostgreSQL based queue – you have the option to switch to a faster one later. Note that pushes currently being sent or waiting in the queue will be lost during the switch.

You can also use separate PostgreSQL instance for push notification queue, which may be beneficial:

config.json
{
...
"push_notifications": {
"enabled": true,
"queue": {
"type": "postgresql",
"postgresql": {
"dsn": "postgresql://postgres:pass@127.0.0.1:5432/push_queue"
}
}
}
}

Configuration reference

This section provides a comprehensive reference for all push notification configuration options.

push_notifications.enabled

Master switch to enable or disable the push notifications feature.

  • Type: bool
  • Default: false

push_notifications.enabled_providers

List of push notification providers to enable.

  • Type: array[string]
  • Valid values: "fcm", "hms", "apns", "webpush"

push_notifications.dry_run

When true, Centrifugo PRO does not send push notifications to providers but prints logs instead. Useful for development.

  • Type: bool
  • Default: false

push_notifications.dry_run_latency

When set together with dry_run, adds artificial delay to workers emulating real-world latency.

  • Type: duration
  • Default: 0s
  • Example: "100ms", "1s"

push_notifications.max_inactive_device_interval

Maximum time interval to keep a device without updates. Devices inactive longer than this will be automatically removed. Set to 0s (default) to keep devices indefinitely.

  • Type: duration
  • Default: 0s
  • Example: "720h" (30 days)

push_notifications.read_from_replica

When true, Centrifugo will use PostgreSQL replicas for read operations where possible. Requires database.postgresql.replica_dsn to be configured.

  • Type: bool
  • Default: false

push_notifications.queue

Queue configuration object. Centrifugo PRO supports Redis Streams (default) or PostgreSQL for push notification queuing.

push_notifications.queue.type

Specifies the queue backend type.

  • Type: string
  • Valid values: "redis", "postgresql"
  • Default: "redis"

push_notifications.queue.redis

Redis queue configuration object. Supports all standard Redis configuration options (address, Sentinel, Cluster, TLS, etc.). See Redis Engine for common Redis options.

FieldTypeDefaultDescription
addressstringRedis server address, e.g. "localhost:6379"
reuse_from_engineboolfalseReuse Redis connection from the engine configuration
consumer_concurrencyint64Number of concurrent consumer workers processing push notification jobs
max_stream_lengthint64100000Maximum length of the Redis Stream. Older entries may be trimmed when limit is reached

push_notifications.queue.postgresql

PostgreSQL queue configuration object. Supports DSN, replica DSN, and TLS configuration.

FieldTypeDefaultDescription
dsnstringPostgreSQL connection string, e.g. "postgresql://user:pass@localhost:5432/dbname"
reuse_from_databaseboolfalseReuse PostgreSQL connection from the database configuration
consumer_concurrencyint16Number of concurrent consumer workers
scheduler_consumer_concurrencyint16Number of concurrent scheduler consumer workers for delayed pushes
prefixstring""Table name prefix for queue-related tables

push_notifications.fcm

FCM (Firebase Cloud Messaging) provider configuration object.

FieldTypeDefaultDescription
credentials_filestringRequired. Path to Firebase service account credentials JSON file
tokens_batch_sizeint500Maximum number of tokens in a single batch request to FCM

push_notifications.hms

HMS (Huawei Messaging Service) provider configuration object.

FieldTypeDefaultDescription
app_idstringRequired. Your HMS application ID
app_secretstringRequired. Your HMS application secret
auth_endpointstringCustom HMS authentication endpoint. Uses HMS default if not set
push_endpointstringCustom HMS push endpoint. Uses HMS default if not set
tokens_batch_sizeint1000Maximum number of tokens in a single batch request to HMS

push_notifications.apns

APNs (Apple Push Notification service) provider configuration object.

FieldTypeDefaultDescription
endpointstring"development"APNs endpoint: "development", "production", or custom https:// URL
bundle_idstringRequired. iOS application bundle identifier
auth_typestringRequired. Authentication method: "token" or "cert"
tokens_batch_sizeint100Maximum number of tokens to process in parallel

Token-based authentication (auth_type: "token", recommended):

FieldTypeDescription
token_key_filestringPath to .p8 authentication key file from Apple Developer portal. Mutually exclusive with token_key_pem
token_key_pemstringPEM-encoded authentication key content (inline). Mutually exclusive with token_key_file
token_key_idstringRequired. 10-character Key ID from Apple Developer account
token_team_idstringRequired. 10-character Team ID from Apple Developer account

Certificate-based authentication (auth_type: "cert"):

FieldTypeDescription
cert_p12_filestringPath to .p12 certificate file. Mutually exclusive with cert_p12_b64
cert_p12_b64stringBase64-encoded .p12 certificate content. Mutually exclusive with cert_p12_file
cert_p12_passwordstringPassword for .p12 certificate (if encrypted)

push_notifications.webpush

Web Push (VAPID) provider configuration object.

FieldTypeDefaultDescription
vapid_public_keystringRequired. base64url-encoded VAPID public (application server) key. Must match the applicationServerKey used on the frontend
vapid_private_keystringRequired. base64url-encoded VAPID private key. Keep it secret
subjectstringRequired. VAPID subject (JWT sub claim) — a mailto: or https: URL identifying the application server contact
tokens_batch_sizeint100Maximum number of subscriptions to send to concurrently
allowed_endpoint_originsarray[string]built-in listAllowed push service origins (glob patterns, same syntax as client.allowed_origins). When empty, a built-in list of the mainstream browser push services is used; when set, it replaces that list. Use * to allow any origin. See Endpoint SSRF protection
extra_allowed_endpoint_originsarray[string][]Origins (same glob syntax) added on top of allowed_endpoint_origins (or the built-in defaults when it is unset). Use it to allow a self-hosted push service while keeping the defaults

Complete configuration example

Here's a comprehensive example showing all providers configured together:

config.json
{
"database": {
"enabled": true,
"postgresql": {
"dsn": "postgresql://postgres:pass@127.0.0.1:5432/postgres",
"replica_dsn": [
"postgresql://postgres:pass@replica-host:5432/postgres"
]
}
},
"push_notifications": {
"enabled": true,
"enabled_providers": ["fcm", "hms", "apns", "webpush"],
"dry_run": false,
"max_inactive_device_interval": "720h",
"read_from_replica": true,
"queue": {
"type": "redis",
"redis": {
"address": "localhost:6379",
"consumer_concurrency": 64,
"max_stream_length": 100000
}
},
"fcm": {
"credentials_file": "/path/to/fcm-credentials.json",
"tokens_batch_size": 500
},
"hms": {
"app_id": "your_app_id",
"app_secret": "your_app_secret",
"tokens_batch_size": 500
},
"apns": {
"endpoint": "production",
"bundle_id": "com.example.app",
"auth_type": "token",
"token_key_file": "/path/to/AuthKey.p8",
"token_key_id": "ABCDE12345",
"token_team_id": "TEAM123456",
"tokens_batch_size": 100
},
"webpush": {
"vapid_public_key": "<your_vapid_public_key>",
"vapid_private_key": "<your_vapid_private_key>",
"subject": "mailto:you@example.com",
"tokens_batch_size": 100
}
}
}

API description

Push notifications of Centrifugo PRO come with a set of additional server API methods.

device_register

Registers or updates device information.

device_register request

FieldTypeRequiredDescription
idstringNoDevice ID. Omit on first registration — Centrifugo generates one and returns it. Pass the stored value on re-registration to update the same device. See Device lifecycle.
providerstringYesProvider of the device token (valid choices: fcm, hms, apns, webpush).
tokenstringYesPush notification token for the device. For webpush, this is the browser PushSubscription object serialized as a JSON string.
platformstringYesPlatform of the device (valid choices: ios, android, web).
userstringNoUser associated with the device.
timezonestringNoTimezone of device user (IANA time zone identifier, ex. Europe/Nicosia). See Timezone aware push
localestringNoLocale of device user. Must be IETF BCP 47 language tag - ex. en-US, fr-CA. See Localizations
topicsarray[string]NoDevice topic subscriptions. This should be a full list which replaces all the topics previously associated with the device. User topics managed by UserTopic model will be automatically attached.
metamap[string]stringNoAdditional custom metadata for the device

device_register result

Field NameTypeRequiredDescription
idstringYesThe device ID that was registered/updated.

device_update

Call this method to update a device. For example, when a user logs out of the app and you need to detach the user ID from the device.

device_update request

FieldTypeRequiredDescription
idsarray[string]NoDevice ids to filter
usersarray[string]NoDevice users filter
user_updateDeviceUserUpdateNoOptional user update object
timezone_updateDeviceTimezoneUpdateNoOptional timezone update object
locale_updateDeviceLocaleUpdateNoOptional locale update object
meta_updateDeviceMetaUpdateNoOptional device meta update object
topics_updateDeviceTopicsUpdateNoOptional topics update object

DeviceUserUpdate:

FieldTypeRequiredDescription
userstringYesUser to set
When to use user_update

user_update rewrites the user identifier on the matched devices — it's meant for administrative/bulk changes where the same person keeps the same device but their user ID string changes (account-ID migration, account merge, backfilling a user onto anonymously-registered devices). It updates the user field only and does not re-sync user-bound topics (the device's topic rows aren't tied to the user identifier, and you'd migrate user_topics bindings separately).

To assign a device to a different user (e.g. a different person logs in on a shared device), use device_register with the new user instead — that copies the new user's topics onto the device in one call. See Device lifecycle and best practices.

DeviceTimezoneUpdate:

FieldTypeRequiredDescription
timezonestringYesTimezone to set

DeviceLocaleUpdate:

FieldTypeRequiredDescription
localestringYesLocale to set

DeviceMetaUpdate:

FieldTypeRequiredDescription
metamap[string]stringYesMeta to set

DeviceTopicsUpdate:

FieldTypeRequiredDescription
opstringYesOperation to make: add, remove or set
topicsarray[string]YesTopics for the operation

device_update result

Empty object.

device_remove

Removes a device from storage. This may also be called when a user logs out of the app and you no longer need the device token.

device_remove request

Field NameTypeRequiredDescription
idsarray[string]NoA list of device IDs to be removed
usersarray[string]NoA list of device user IDs to filter devices to remove

device_remove result

Empty object.

device_list

Returns a paginated list of registered devices according to request filter conditions.

device_list request

FieldTypeRequiredDescription
filterDeviceFilterYesHow to filter results
cursorstringNoCursor for pagination (last device id in previous batch, empty for first page).
limitint32NoMaximum number of devices to retrieve.
include_total_countboolNoFlag indicating whether to include total count for the current filter.
include_topicsboolNoFlag indicating whether to include topics information for each device.
include_metaboolNoFlag indicating whether to include meta information for each device.
include_webpush_keysboolNoFlag indicating whether to include webpush_keys for each device (webpush only).

DeviceFilter:

FieldTypeRequiredDescription
idsarray[string]NoList of device IDs to filter results.
providersarray[string]NoList of device token providers to filter results.
platformsarray[string]NoList of device platforms to filter results.
usersarray[string]NoList of device users to filter results.
topicsarray[string]NoList of topics to filter results.

device_list result

Field NameTypeRequiredDescription
itemsarray[Device]YesA list of devices
next_cursorstringNoCursor string for retrieving the next page, if not set - then no next page exists
total_countintegerNoTotal count value (if include_total_count used)

Device:

Field NameTypeRequiredDescription
idstringYesThe device's ID.
providerstringYesThe device's token provider.
tokenstringYesThe device's token. For webpush this is the subscription endpoint (its stable identity).
platformstringYesThe device's platform.
userstringNoThe user associated with the device.
topicsarray[string]NoOnly included if include_topics was true
metamap[string]stringNoOnly included if include_meta was true
webpush_keysstringNoWeb Push subscription keys JSON ({p256dh, auth}). Only included if include_webpush_keys was true

Two kinds of topic lists

There are two separate topic lists, and it's important to know which is which:

  • User topics (user_topic_update / user_topic_list) — the list of topics a user should follow. Think of this as your intent: "this user wants these topics."
  • Device topics (device_topic_update / device_topic_list) — the list of topics a specific device is actually subscribed to. This is the list Centrifugo reads when you send to a topic — it decides who gets the push.

How they relate: when a device is registered for a user, Centrifugo copies that user's topics into the device's list (along with any topics you pass in the device_register call). So the user list is the convenient place to manage subscriptions once per user, and the device list is the result that actually drives delivery.

How they stay in sync: user_topic_update updates the per-user list and immediately applies the change to that user's already-registered devices, so the device list reflects it right away. Two cases catch up at the next device_register instead: a device that hasn't registered yet, and the global "" binding that applies to every user. A topic send always follows the device list.

So: use user topics to manage "who follows what", and use device topics (especially device_topic_list) to check or debug what a given device will really receive.

device_topic_update

Manage mapping of device to topics.

device_topic_update request

FieldTypeRequiredDescription
device_idstringYesDevice ID.
opstringYesadd or remove or set
topicsarray[string]NoList of topics.
userstringNoOptional ownership guard. If set, the update is applied only if the device currently belongs to this user. If the device exists but is owned by someone else, the request fails with a conflict error; if the device doesn't exist, it fails with a not found error. Nothing is changed in either case. Use it to avoid landing one user's topics on a device that has changed hands.

device_topic_update result

Empty object.

tip

Manage topics that belong to a user via user_topic_update — those follow the device's current owner automatically. Use device_topic_update for topics that belong to the device itself regardless of who is logged in. If you do target a device directly for user-specific topics, pass user as a guard so a stale device→user assumption can't leak topics across users.

device_topic_list

List device to topic mapping.

device_topic_list request

FieldTypeRequiredDescription
filterDeviceTopicFilterNoList of device IDs to filter results.
cursorstringNoCursor for pagination (last device id in previous batch, empty for first page).
limitint32NoMaximum number of devices to retrieve.
include_deviceboolNoFlag indicating whether to include Device information for each object.
include_total_countboolNoFlag indicating whether to include total count info to response.

DeviceTopicFilter:

FieldTypeRequiredDescription
device_idsarray[string]NoList of device IDs to filter results.
device_providersarray[string]NoList of device token providers to filter results.
device_platformsarray[string]NoList of device platforms to filter results.
device_usersarray[string]NoList of device users to filter results.
topicsarray[string]NoList of topics to filter results.
topic_prefixstringNoTopic prefix to filter results.

device_topic_list result

Field NameTypeRequiredDescription
itemsarray[DeviceTopic]YesA list of DeviceTopic objects
next_cursorstringNoCursor string for retrieving the next page, if not set - then no next page exists
total_countintegerNoTotal count value (if include_total_count used)

DeviceTopic:

FieldTypeRequiredDescription
idstringYesID of DeviceTopic object
device_idstringYesDevice ID
topicstringYesTopic

user_topic_update

Manage the per-user topic list. Updating it immediately applies the change to the user's already-registered devices (and a device registered later picks up the current list at registration). The global "" binding — which applies to every user — is the one case applied at registration rather than immediately. See Device lifecycle.

user_topic_update request

FieldTypeRequiredDescription
userstringYesUser ID.
opstringYesadd or remove or set
topicsarray[string]NoList of topics.

user_topic_update result

Empty object.

user_topic_list

List user to topic mapping.

user_topic_list request

FieldTypeRequiredDescription
filterUserTopicFilterNoFilter object.
cursorstringNoCursor for pagination (last id in previous batch, empty for first page).
limitint32NoMaximum number of UserTopic objects to retrieve.
include_total_countboolNoFlag indicating whether to include total count info to response.

UserTopicFilter:

FieldTypeRequiredDescription
usersarray[string]NoList of users to filter results.
topicsarray[string]NoList of topics to filter results.
topic_prefixstringNoTopic prefix to filter results.

user_topic_list result

Field NameTypeRequiredDescription
itemsarray[UserTopic]YesA list of UserTopic objects
next_cursorstringNoCursor string for retrieving the next page, if not set - then no next page exists
total_countintegerNoTotal count value (if include_total_count used)

UserTopic:

FieldTypeRequiredDescription
idstringYesID of UserTopic
userstringYesUser ID
topicstringYesTopic

send_push_notification

Send push notification to specific device_ids, or to topics, or native provider identifiers like fcm_tokens, or to fcm_topic. The request will be queued by Centrifugo, consumed by Centrifugo built-in workers, and sent to the provider API.

send_push_notification request

Field nameTypeRequiredDescription
recipientPushRecipientYesRecipient of push notification
notificationPushNotificationYesPush notification to send
uidstringNoUnique identifier for each push notification request, can be used to cancel push. We recommend using UUID v4 for it. Two different requests must have different uid
send_atint64NoOptional Unix time in the future (in seconds) when to send push notification, push will be queued until that time.
optimize_for_reliabilityboolNoMakes processing heavier, but handles edge cases — for example, it avoids losing pushes that are mid-send if the queue is briefly unavailable.
limit_strategyPushLimitStrategyNoCan be used to set push time constraints (based on device timezone) and rate limits. Note, when it's used Centrifugo processes pushes one by one instead of batch sending
analytics_uidstringNoIdentifier for push notification analytics, if not set - Centrifugo will use uid field.
localizationsmap[string]PushLocalizationNoOptional per language localizations for push notification.
use_templatingboolNoIf set - Centrifugo will use templating for push notification. Note that setting localizations enables templating automatically.
use_metaboolNoIf set - Centrifugo will additionally load device meta during push sending, this meta becomes available in templating.

PushRecipient (you must set only one of the following fields):

FieldTypeRequiredDescription
filterDeviceFilterNoSend to device IDs based on Centrifugo device storage filter
fcm_tokensarray[string]NoSend to a list of FCM native tokens
fcm_topicstringNoSend to a FCM native topic
fcm_conditionstringNoSend to a FCM native condition
hms_tokensarray[string]NoSend to a list of HMS native tokens
hms_topicstringNoSend to a HMS native topic
hms_conditionstringNoSend to a HMS native condition
apns_tokensarray[string]NoSend to a list of APNs native tokens
webpush_tokensarray[string]NoSend to a list of raw Web Push subscriptions (each item is a PushSubscription JSON string)

PushNotification:

FieldTypeRequiredDescription
expire_atint64NoUnix timestamp when Centrifugo stops attempting to send this notification. Note, it's Centrifugo specific and does not relate to notification TTL fields. We generally recommend to always set this to a reasonable value to protect your app from old push notifications sending
fcmFcmPushNotificationNoNotification for FCM
hmsHmsPushNotificationNoNotification for HMS
apnsApnsPushNotificationNoNotification for APNs
webpushWebPushPushNotificationNoNotification for Web Push

FcmPushNotification:

FieldTypeRequiredDescription
messageJSON objectYesFCM Message described in FCM docs.

HmsPushNotification:

FieldTypeRequiredDescription
messageJSON objectYesHMS Message described in HMS Push Kit docs.

ApnsPushNotification:

FieldTypeRequiredDescription
headersmap[string]stringNoAPNs headers
payloadJSON objectYesAPNs payload

WebPushPushNotification:

FieldTypeRequiredDescription
headersmap[string]stringNoWeb Push HTTP headers. Recognized keys: TTL (seconds to retain for offline devices, default 4 weeks), Urgency (very-low/low/normal/high), Topic (collapse key)
payloadJSON objectYesArbitrary JSON payload delivered to the browser service worker (received via event.data.json() in the push event)

PushLocalization:

FieldTypeRequiredDescription
translationsmap[string]stringYesVariable name to value for the specific language.

PushLimitStrategy:

FieldTypeRequiredDescription
rate_limitPushRateLimitStrategyNoSet rate limit policies
time_limitPushTimeLimitStrategyNoSet time limit policy

PushRateLimitStrategy:

FieldTypeRequiredDescription
keystringNoOptional key for rate limit policy, supports variables (device.id and device.user).
policiesarray[RateLimitPolicy]NoArray of rate limit policies to apply
drop_if_rate_limitedboolNoDrop push if rate limited, otherwise queue for later

RateLimitPolicy:

FieldTypeRequiredDescription
rateintYesAllowed rate
interval_msintYesInterval over which rate is allowed

PushTimeLimitStrategy:

FieldTypeRequiredDescription
send_after_timestringYesLocal time in format HH:MM:SS after which push must be sent
send_before_timestringYesLocal time in format HH:MM:SS before which push must be sent
no_tz_send_nowboolNoIf device does not have timezone send push immediately, by default - will be dropped

send_push_notification result

Field NameTypeDescription
uidstringUnique send id, matches uid in request if it was provided

cancel_push

Cancel delayed push notification (which was sent with custom send_at value).

cancel_push request

FieldTypeRequiredDescription
uidstringYesuid of push notification to cancel

cancel_push result

Empty object.

update_push_status

This API call is experimental, some changes may happen here.

Centrifugo PRO also allows tracking status of push notification delivery and interaction. It's possible to use update_push_status API to save the updated status of push notification to the notifications analytics table. Then it's possible to build insights into push notification effectiveness by querying the table.

The update_push_status API supposes that you are using uid field with each notification sent and you are using Centrifugo PRO generated device IDs (as described in steps to integrate).

This is part of the server API at the moment, so you need to send these requests from your backend. We can consider making this API suitable for requests from the client side – please reach out if your use case requires it.

update_push_status request

FieldTypeRequiredDescription
analytics_uidstringYesanalytics_uid from send_push_notification
statusstringYesStatus of push notification - delivered or interacted
device_idstringYesDevice ID
msg_idstringNoOptional Message ID of push notification issued by the provider

update_push_status result

Empty object.

Timezone aware push

Setting timezone on a device (see device_register call) opens the way for timezone-aware push notifications. This is nice because you can send notifications to users at a convenient time of day — avoid pushes at night, push at a specific time.

To send such push notifications use time_limit field of PushLimitStrategy. For example, you can send push between 09:00:00 and 09:30:00 – and Centrifugo will send push somewhere during this period of user's local time.

tip

Given Centrifugo takes timezone from devices table into account timezone aware pushes only work with requests where DeviceFilter is used for sending – i.e. when Centrifugo iterates over devices in the database. If you send using raw tokens and want to inherit possibility to use timezones - reach out to us, this may be supported.

Templating

It's possible to use templating in the content of your push notification payloads. By default, Centrifugo does not use templating since this allows broadcasting pushes at maximum speed. You have to set the use_templating flag to true when sending a push to enable template execution. Here is an example of using templating:

{
..
"title": "Hello {{.device.meta.first_name}}"

To access device meta content in a push template (as shown above), additionally set the use_meta flag to true in the send push notification request. Without use_meta you only have access to .device.id and .device.user variables.

tip

Templating only works with requests where DeviceFilter is used for sending – i.e. when Centrifugo iterates over devices in the database.

Localizations

Templating also allows us to localize push notification content based on device locale (see device_register call).

When sending push notification use localizations field of send_push_notification request:

{
..
"localizations": {
"pt": {
"translations": {
"greeting": "Olá",
"question": "Como tá indo"
}
},
"fr": {
"translations": {
"greeting": "Bonjour",
"question": "Comment ça va"
}
}
}
}

In push payload you can then use templating and l10n object will be set to a proper translation map based on device locale:

{
..
"title": "{{default [[hello]] .l10n.greeting}}! {{ default [[How is it going]] .l10n.question }} ?"

So that a device with pt-BR locale will get a push notification with title Olá! Como tá indo?.

Note, it's required to set a default value here (we used English in the example) for cases when no locale is found for the device, or no translations for the device language are provided in the request.

Push rate limits

A good practice when working with push notifications is to avoid sending too many notifications to your users, especially marketing ones. Centrifugo PRO provides a way to rate limit notifications at the user's device level.

To do this, use the rate_limit field of PushLimitStrategy. For example, you can configure policies to send push notifications no faster than once per minute and no more than 10 pushes in one hour. Centrifugo supports several policies for a rate limit strategy. If a push notification hits the provided rate limits, it will be automatically delayed, or dropped if the drop_if_rate_limited flag is set to true.

tip

Given Centrifugo takes timezone from devices table into account timezone aware pushes only work with requests where DeviceFilter is used for sending – i.e. when Centrifugo iterates over devices in the database. If you send using raw tokens and want to inherit possibility to use rate limits - reach out to us, this may be supported.

Exposed metrics

Several metrics are available to monitor the state of Centrifugo push worker system:

centrifugo_push_notification_count

  • Type: Counter
  • Labels: provider, recipient_type, platform, success, err_code
  • Description: Total count of push notifications.
  • Usage: Helps in tracking the number and success rate of push notifications sent, providing insights for optimization and troubleshooting.

centrifugo_push_queue_consuming_lag

  • Type: Gauge
  • Labels: provider, queue
  • Description: Queue consuming lag in seconds.
  • Usage: Useful for monitoring the delay in processing jobs from the queue, helping identify potential bottlenecks and ensuring timely processing.

centrifugo_push_consuming_inflight_jobs

  • Type: Gauge
  • Labels: provider, queue
  • Description: Number of jobs currently being processed.
  • Usage: Helps in tracking the load on the job processing system, ensuring that resources are being utilized efficiently.

centrifugo_push_job_duration_seconds

  • Type: Summary
  • Labels: provider, recipient_type
  • Description: Duration of push processing job in seconds.
  • Usage: Useful for monitoring the performance of job processing, helping in performance tuning and issue resolution.

Further reading and tutorials

Some additional materials include: