Push notifications look simple from the product surface: a service sends a message and a device displays it. The engineering reality is more involved. Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM) sit between your application and the user's device, each with its own registration, authentication, payload, delivery, and failure considerations.
The most reliable approach is to treat push as a distributed system rather than a single API call. Your application must obtain and maintain a valid device address, associate that address with the right user, send messages safely, handle provider responses, and make sensible decisions when delivery is delayed or rejected.
Understand the delivery path
A typical notification passes through several components:
- Your product backend decides that a notification is needed.
- Your notification service selects one or more targets.
- APNs or FCM accepts, rejects, or queues the provider request.
- The platform delivers the message to an eligible device or browser.
- The client application interprets the payload and performs an action.
Each stage can fail independently. A successful server response does not necessarily mean that a person saw the notification. It may only mean that the provider accepted the request for further processing.
Before choosing an implementation, clarify what your team needs to know at each stage. Can you distinguish an accepted request from a displayed notification? Which delivery and open events are available in your client environment? How will you investigate a notification that was accepted but never acted upon? Verify the current APNs and FCM documentation for the exact response and event semantics relevant to your platforms.
Tokens are identifiers, not permanent identities
The device token or registration token is an address supplied by the platform. It should not be treated as a permanent identifier for a physical device, installation, or person. Tokens can change when an application is reinstalled, its data is cleared, the operating environment changes, or the provider rotates credentials.
Your client integration should register for notifications, send the resulting token to your backend, and repeat that process when the platform reports a change. The backend should store the token with useful context, such as:
- Application and environment.
- Platform and application version.
- User or account association, where applicable.
- Last-seen time and registration status.
- Permission or authorisation state where your client can provide it.
Do not assume that one user has one token. A user may have several phones, tablets, browsers, or application installations. Conversely, a token should not automatically be considered a stable representation of the user. Keep user identity and device addressing as separate concepts.
A good registration flow is idempotent. Repeated registration of the same current token should update existing data instead of creating an uncontrolled collection of duplicates. When a token becomes invalid, mark it inactive or remove it according to your retention and audit requirements.
Keep APNs and FCM differences behind a boundary
Teams often begin with provider-specific code in business workflows: one branch for Apple devices and another for Android or web clients. This can work initially, but it makes retries, logging, targeting, and future provider changes harder to manage.
A better design separates three concerns:
- Business intent, such as “tell the user that an order is ready”.
- A normalised notification model, including title, body, target, data, and priority decisions.
- Provider-specific delivery, authentication, payload construction, and error handling.
The normalised model should not pretend that every platform behaves identically. Instead, it should support a common baseline and allow explicit platform overrides. For example, an interaction may need a platform-specific action, sound, badge, expiry policy, or background handling instruction. Keep those differences visible and tested rather than silently dropping them.
When comparing APNs and FCM for a new project, ask which client platforms you support, which authentication model your operations team can manage, what payload features you require, and how each provider represents invalid or expired registrations. Confirm those details in the current provider documentation before committing your abstraction to them.
Targeting requires a data model
Sending to a single device is only one use case. Product teams usually need to address a user, a group of users, a topic or tag, a segment, or every enabled device for a carefully controlled operational message.
The target model should answer several questions:
- Is this message for one installation, one account, or a group?
- Should all of a user's active devices receive it?
- Can a user opt out of this category without disabling every notification?
- How are stale devices removed from a segment?
- What prevents an administrative broadcast from reaching the wrong audience?
Do not build audience selection by placing large device-token lists directly in every business event. Store audience membership separately, apply permission and preference checks, and generate a delivery plan. For sensitive notifications, record why a target was selected and which policy allowed the send.
HoneyNotify's server-side send model uses POST /v1/notifications with a Bearer API key and an Idempotency-Key. A notification requires a title, body, and target. Targets can address a device, user, tag, segment, or all enabled devices. This gives a notification service a useful boundary: product code chooses intent and audience, while the delivery layer handles registered devices and provider-specific routes.
Make retries safe
Network timeouts create an awkward state: your backend may not know whether the provider received the request. Retrying without protection can produce duplicate notifications. Never assume that a timeout means the first request failed.
Use an idempotency key for each logical send. Persist it alongside the business event and notification status, and reuse it when retrying the same operation. Do not generate a new key for every network attempt unless you intentionally want separate sends.
Also separate transient failures from permanent failures. A temporary connectivity problem may justify a bounded retry with backoff. An invalid credential, malformed payload, or invalid token needs a different response. Record provider responses in a way that supports diagnosis, but avoid logging secrets or unnecessary personal data.
Consider expiry as well. A notification about a rapidly changing state may be useless after a short period, while a security alert may need a different policy. Confirm which expiry and priority controls are available for each provider and platform, then define defaults instead of letting every feature invent its own behaviour.
Design the client payload deliberately
A push payload should contain enough information for the client to decide what to do, not a large copy of data that may already be stale. A common pattern is to include a notification type and a stable resource identifier, then let the application fetch current data after the user interacts with the message.
Keep payload handling defensive:
- Treat all incoming values as untrusted input.
- Validate notification types and resource identifiers.
- Make handlers safe to run more than once.
- Handle a user opening an old notification after the underlying state has changed.
- Avoid placing secrets or excessive personal data in payloads.
- Define behaviour for foreground, background, and terminated application states.
Ask whether the message should be visible, silent, or both. Do not assume that a background request will always run immediately. Platform power management, permission state, connectivity, and application lifecycle rules can affect what happens. Verify current platform behaviour for the exact client SDK versions you support.
Test the failure paths
A notification feature is incomplete if it has only been tested on one developer device. Build a test matrix covering fresh installation, existing installation, denied permission, revoked permission, token change, offline operation, application upgrade, multiple devices for one user, and a user signing out.
Test provider and routing failures too:
- Invalid or expired credentials.
- Invalid registration data.
- Malformed or unsupported payload fields.
- Provider timeouts and rate limits.
- Duplicate business events.
- A target containing both active and stale devices.
- A user who changes notification preferences between selection and send.
Client SDKs should cover device registration, identity, token lifecycle, payload handling, and lifecycle events across iOS, Android, and Web Push. Make those events observable in development and test environments. A registration dashboard that shows the current platform, token state, last-seen time, and association can save considerable debugging time.
Common mistakes
- Treating a provider acceptance response as proof of display or user engagement.
- Storing one token per user and silently losing other active devices.
- Failing to update a token when the client reports a change.
- Mixing sandbox and production environments without explicit tracking.
- Retrying a timed-out request with a new identity and creating duplicates.
- Embedding provider-specific payload rules throughout product code.
- Sending sensitive content in a payload without considering device visibility.
- Leaving stale tokens in audiences and interpreting repeated failures as user behaviour.
- Assuming Android, iOS, and web lifecycle handling are interchangeable.
If you migrate from another notification provider, map identifiers carefully. Imported OneSignal subscription IDs can remain as device IDs when matching provider tokens later register, but token rotation or another provider environment can prevent matching. Plan a reconciliation process rather than assuming historical identifiers will always join to future registrations.
A practical implementation sequence
Start with a registration contract between the client and backend. Define how identity, platform, environment, token, preferences, and last-seen information are represented. Next, create a normalised notification command with an idempotency key and an auditable target.
Then implement provider adapters with explicit response classification. Add structured logs and metrics for requested, accepted, rejected, retried, expired, and opened states where those events are available. Finally, test lifecycle changes and failure recovery before adding broad audience features.
Conclusion
APNs and FCM are important delivery components, but dependable push notifications depend on the surrounding system. Model tokens as changeable addresses, separate users from installations, isolate provider-specific behaviour, make sends idempotent, and test lifecycle and failure paths. With those foundations in place, your team can support multiple platforms without allowing provider details to leak into every product feature.
