Somewhere in most product specs there is a line that reads “send a push notification when X happens”. It looks like an afternoon of work. In practice it is a small system with its own failure modes, its own vendor dependencies, and its own user experience decisions that are hard to reverse once people have opted in or out. If your product depends on someone actually seeing a reminder, an alert or a status change, it is worth understanding what that line in the spec is actually asking an engineer to build.
Web push, native push, or both
There are two largely separate delivery paths, and a product that needs to reach people reliably often ends up building both. Native push goes through Apple’s APNs or Google’s FCM, requires an installed app, and gives you the most consistent delivery on phones. Web push goes through the browser vendor’s own push service and can reach a desktop or an Android phone without an app install, but it is a second, different integration with its own token format and its own expiry behaviour, and Apple only added meaningful support for it in Safari relatively recently, with its own quirks around how the site has to be added to the home screen first.
The decision is not “which is better”, it is which one matches how people actually use the product. A shift-scheduling tool where staff check a phone app benefits from native push. A browser-based dashboard that an operations manager keeps open on a desktop all day gets more value from web push, or arguably from nothing more elaborate than an in-app badge. Building both because it feels safer doubles the integration surface, the token management, and the number of places a message can silently fail to arrive.
Delivery is best-effort, not guaranteed
This is the point that catches product owners out. APNs and FCM are both best-effort delivery networks. A device that is switched off, offline, or has been asleep long enough can miss a notification entirely, and neither Apple nor Google gives you a confirmed-delivered receipt in the way an email or SMS provider does. For a marketing nudge that is a non-issue. For a rota change, a security alert, or anything where a missed message has a real consequence, treating push as the only channel is a design mistake, not a technical one.
The fix is usually a fallback tier rather than a more clever push implementation: push first, then an in-app inbox the user sees on next open so nothing is ever truly lost, and for genuinely critical alerts an email or SMS escalation if the push has not been acknowledged within a defined window. That escalation logic, and the state tracking behind it, is normally a larger piece of engineering than the notification send itself.
Permission is a one-shot negotiation
Both iOS and modern browsers ask the user for permission before any push notification can be sent, and on most platforms there is no native way to ask again once someone has said no. That makes the moment you trigger the permission prompt a genuine product decision, not a technical default to leave at “ask on first launch”. Asking before someone has done anything in the product gets a reflexive decline. Asking right after the action that the notification would actually be useful for, such as scheduling a first shift or setting a first reminder, gets a meaningfully different response, because the value is obvious at the moment of asking.
Worth deciding explicitly at the spec stage: what happens to a user who declines. If the product’s core workflow silently assumes notifications are on, a decline turns into a support ticket later that looks like a bug. Designing the in-app fallback for the declined state up front is cheaper than retrofitting it.
Quiet hours and time zones are not an edge case
Any product sending notifications tied to a schedule, whether that is a family reminder, a shift alert, or a maintenance window on a piece of equipment, needs a real answer to “what time zone is this evaluated in”, and it needs quiet-hours logic that respects the recipient, not the server. A notification scheduling system that stores everything in UTC and fires without checking the recipient’s local time will, at some point, wake someone at 3am, and it will do it reliably enough that it becomes the first thing users complain about. This is cheap to get right if it is designed in from the start, and awkward to bolt on afterwards because it usually means re-touching every place a notification is queued.
Build the sending logic, buy the delivery infrastructure
Talking directly to APNs and FCM is possible and gives full control, but it means managing device tokens, handling token refresh and expiry, and building your own retry and batching logic. Firebase Cloud Messaging (free, and it can also front APNs for you), AWS SNS or AWS Pinpoint, and dedicated providers such as OneSignal all exist to remove that layer. For most products the sensible split is: buy the transport, build the decision logic that decides what to send, to whom, and when, since that logic is specific to your product and is where the actual value sits.
The trade-off to weigh honestly is lock-in against speed. A managed provider gets a working notification system live in days. It also means your notification history, your targeting rules and sometimes your user segmentation live in a third-party dashboard rather than your own database. For a product where notifications are a supporting feature, that trade is usually worth it. For a product where notification behaviour is close to the core value, such as an alerting platform, keeping the decision logic and the audit trail in your own system from day one avoids a migration later.
Testing this properly is unglamorous and easy to skip
Notifications behave differently depending on whether the app is in the foreground, backgrounded, or force-quit, and that state affects whether your code even runs to display the message or whether the OS handles it entirely. Device tokens expire and rotate, and a production system needs to detect a dead token and stop sending to it rather than silently accumulating failed sends. None of this shows up in a quick demo where the developer’s own phone is unlocked and the app is open. It shows up three weeks after launch, as a slow trickle of “I didn’t get notified” reports that are individually hard to reproduce. Budgeting real device testing across foreground, background and killed states, on both platforms, before launch is the difference between finding this in testing or finding it in support.
Before you commission this feature
- Decide which channel (native push, web push, email, SMS) actually matches how your users use the product, rather than defaulting to all of them.
- Classify each notification type by consequence of non-delivery, and give the ones that matter a fallback channel and an acknowledgement check.
- Design the permission prompt’s trigger moment and the declined-state experience as part of the spec, not as an afterthought.
- Store and evaluate schedules in the recipient’s time zone, with quiet hours as a first-class rule, not a later patch.
- Choose a managed provider for transport unless notification behaviour is core to your product’s value, in which case keep the decision logic in-house from the start.
- Ask how the team plans to test foreground, background and killed-app states on real devices before launch, and treat a vague answer as a red flag.