React Native Push Notifications for IoT Alerts in Expo

React Native Push Notifications for IoT Alerts in Expo

Dec 12, 2025
11 minute read

A low battery warning at noon is useful. The same warning every 10 minutes becomes noise. That tension is the hard part of React Native push notifications for IoT apps, not the API call that sends them.

If you're building with Expo, you can ship a solid alert system without leaving the managed path. You still need good event rules, careful payloads, and a backend that treats push as a delivery channel, not the source of truth.

Why IoT alerts need a different notification strategy

IoT alerts aren't chat messages. They describe changing physical states, and those states may last for minutes, hours, or days. A freezer stays warm. A lock stays open. A sensor stays offline. That means your app can't fire a new push every time telemetry lands.

Most teams do better when they split alerts into two layers. The first layer is the device state in your backend. The second layer is the user-facing notification. Once you keep those separate, a lot of messy behavior becomes easier to control.

A low battery event is a good example. The device may report 14 percent battery several times. Your backend should store one open alert, update its timestamp, and suppress repeat pushes until the state changes enough to matter. Then, if the battery drops from 14 percent to 5 percent, you can escalate.

Offline alerts need even more care because networks wobble. A door sensor that misses one heartbeat should not wake someone at 3 a.m. Most apps wait for a grace window, then send one offline alert, then keep the alert open until the device checks back in.

Security events sit at the other end of the scale. If a lock is forced open, delay hurts. You want a short message, high priority, and a tap target that lands on the exact alert record.

Push delivery is the last mile. Your backend alert state is the system that users trust.

That idea shapes every technical choice that follows.

How Expo fits the notification path in 2026

For most Expo apps in 2026, the default stack is still expo-notifications plus Expo Push Tokens. The app gets a token, your backend stores it, and your server sends messages through Expo's push service. Expo then routes them to APNs on iOS and FCM on Android.

That path is a good fit for many IoT apps because it keeps the client simple. You use one API for local and remote notifications, and you avoid a lot of native setup early on. Expo's current setup also expects a development build and a real device for reliable push testing, so plan that into your first sprint.

The full alert path usually looks like this in production: device event -> cloud ingest -> rules engine -> alert record -> notification decision -> Expo Push API -> APNs or FCM -> mobile app. If your phone learns about an event locally, such as a nearby BLE accessory losing connection while the app is open, you can also trigger a local notification instead of waiting for the server.

!A person holds a sleek smartphone showing a smart home alert on the lock screen. The background features a softly blurred, minimalist living room with elegant furniture and warm lighting.

Expo is strongest when your needs are standard but solid: permission handling, remote push, local alerts, tap responses, and deep linking. If you want a broader mobile refresher outside the Expo angle, this React Native push notification guide covers the larger FCM and APNs picture.

The key tradeoff is control. Expo smooths out the common path. Direct APNs and FCM give you more knobs, but they also give you more surface area to own.

Setting up expo-notifications in an Expo app

Start with the basics: expo-notifications, expo-device, and expo-constants. Then wire registration into a screen where the user already sees value, such as after sign-in or after pairing a device. Asking for permission on first launch usually hurts opt-in rates because the request has no context.

Your client needs four jobs. It should ask permission, fetch the Expo push token with the app projectId, define how notifications display, and handle both foreground receipt and tap actions. On Android, create channels before the first alert so users can control sound and importance by category.

import * as Device from "expo-device";
import * as Notifications from "expo-notifications";
import Constants from "expo-constants";
import { Platform } from "react-native";


Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: false,
}),
});


export async function registerForPushAsync() {
if (!Device.isDevice) return null;


if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("security", {
name: "Security alerts",
importance: Notifications.AndroidImportance.MAX,
sound: "default",
});


await Notifications.setNotificationChannelAsync("device-health", {
name: "Device health",
importance: Notifications.AndroidImportance.DEFAULT,
});
}


const existing = await Notifications.getPermissionsAsync();
let status = existing.status;


if (status !== "granted") {
const requested = await Notifications.requestPermissionsAsync();
status = requested.status;
}


if (status !== "granted") return null;


const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;


const token = await Notifications.getExpoPushTokenAsync({ projectId });
return token.data;
}

After registration, send the token to your backend with the user ID, platform, app version, and a timestamp. That record should also map to device ownership or site membership. In an IoT app, one user may follow ten sensors while one sensor may notify five users.

Tap handling matters as much as delivery. Put a route hint and alert ID in the payload, then navigate straight to the device or alert detail screen. If the app opens from a killed state, load the alert record from your backend before you render the detail view. That avoids stale payload data and keeps the app honest.

Design payloads for IoT alerts, not generic pushes

A good payload is short, stable, and safe to show on a lock screen. The body should tell the user what happened and what needs attention. The data object should carry the app logic.

Most teams benefit from a shared schema. Include fields like alertId, deviceId, type, severity, dedupeKey, occurredAt, route, and ackRequired. If the same event may repeat, the dedupeKey should be derived from the condition, not the single telemetry sample.

This table gives a practical starting point.

| Alert type | Default urgency | Example TTL | Typical user action | | ---------------- | --------------- | ----------- | ----------------------------- | | Low battery | Medium | 6 hours | Plan replacement or charging | | Offline device | Medium | 15 minutes | Check power, network, gateway | | Threshold breach | High | 5 minutes | Inspect device or environment | | Security event | Highest | 1 minute | Open app now and review |

These values aren't universal, but they fit many production apps. The pattern matters more than the exact number.

const message = {
to: expoPushToken,
title: "Freezer temperature alert",
body: "Kitchen freezer stayed above 8C for 10 minutes.",
sound: "default",
priority: "high",
channelId: "security",
data: {
alertId: "alt_9f2e",
deviceId: "dev_freezer_12",
type: "threshold_breach",
severity: "high",
dedupeKey: "dev_freezer_12:temp_high",
occurredAt: "2026-06-15T08:11:22Z",
route: "/devices/dev_freezer_12/alerts/alt_9f2e",
ackRequired: true
}
};

For low battery, don't send every 1 percent drop. Pick thresholds that match real action, such as 20 percent, 10 percent, and 5 percent. Store the last sent band so you only notify on band changes.

For offline devices, wait for a missed-heartbeat window and set one open alert per device. If the device stays offline, update backend state without sending another push unless the outage crosses a larger threshold, such as 1 hour or 24 hours.

Threshold breaches work best with hysteresis. If a temperature limit is 8C, you might open the alert at 8C for 10 minutes and clear it only after the reading returns below 6C for a set period. That prevents rapid open-close cycles.

Security events need the shortest path. Use a direct title, minimal body text, and a deep link into the event detail. Keep sensitive location, camera, or user data out of the visible message whenever possible.

iOS and Android don't treat alerts the same way

Android gives you more display control through notification channels. That's a win for IoT apps because "security," "device health," and "maintenance" should not feel the same. If every alert uses a max-importance channel, users will mute the whole app.

iOS takes a different route. Users grant notification permission once, then system behavior such as Focus and scheduled summaries can affect timing. If an alert is time-sensitive, use the matching interruption level where your stack supports it. For the highest-risk cases, Apple offers Critical Alerts, but that requires a separate entitlement and a much tighter review path.

Background behavior also differs. A remote push may wake the app for light work in some cases, but you should never depend on that for core device logic. The safer rule is simple: push tells the user something changed, and the app fetches fresh state after open or tap.

Local notifications still have a place. If your Expo app is connected to a BLE sensor and detects a failure while it's active, a local notification can feel faster than a round-trip through the cloud. Remote push stays the better option when the server sees the event first.

One more detail catches teams off guard. iOS users often care more about lock screen privacy, while Android users often care more about channel noise. Plan for both from the first release.

When Expo managed is enough, and when you need native code or a custom backend

Expo managed is enough for many IoT products. If your app needs remote pushes, local alerts, Android channels, tap navigation, and a sane permission flow, expo-notifications is often all you need on the client side.

The story changes when you need deep platform control. Teams usually move beyond the default path when they need iOS service extensions, Apple Critical Alerts, vendor-specific Android behavior, advanced background execution, or direct FCM and APNs features that Expo doesn't expose cleanly. At that point, a custom development client, native modules, or direct platform integrations start to make sense.

Your backend is where most serious IoT alert work belongs anyway. It should create alert records, map users to devices, apply rate limits, respect preferences, and store send outcomes. It should also poll push receipts, disable bad tokens, and retry with idempotency so one flaky network hop doesn't create duplicate alarms.

A small Node service is enough to start. A serverless path works too. If you want a similar pattern in a different stack, this Expo with Lambda and DynamoDB walkthrough shows how teams wire real-time sends into cloud functions.

If you're starting from an Expo-first IoT boilerplate like IoTfast, keep your device transport layer and your notification layer separate. BLE, MQTT, and push shouldn't know too much about each other. That split makes it easier to replace Expo Push with direct FCM or APNs later if product needs change.

Reduce noise, protect privacy, and improve delivery reliability

Reliable delivery starts before the push leaves your server. Build a dedupe rule that uses device ID, alert type, and state window. Then store one open alert per condition until the condition clears. If the same event repeats while the alert is open, update the record and skip the push.

User controls matter just as much. Give people per-device or per-alert-type settings, plus quiet hours for non-urgent events. A maintenance warning can wait until morning. A door forced open should not.

A strong preference model often includes:

  • Separate toggles for security, health, and maintenance alerts.
  • Quiet hours for low-priority events.
  • Digest mode for repeated non-urgent notices.
  • Per-device mute when a sensor is under repair.
  • A fallback channel, such as email, for users who disable push.

Teams tuning alert volume can borrow ideas from push notification optimization guidance, even though IoT use cases need stricter event logic than marketing apps.

Privacy needs the same discipline. Don't put a full street address, door code, raw camera metadata, or health reading in the visible body. Use generic copy like "Security alert at home" or "Freezer needs attention," then fetch full details after open. Keep payload data small, encrypt data in transit and at rest on your backend, and verify that incoming device events are signed or otherwise authenticated.

Finally, monitor the boring parts. Track token age, receipt failures, opt-out rates, and open alert counts per device. Those numbers tell you when your React Native push notifications are helping users, and when they're training users to ignore the app.

Conclusion

Expo gives IoT teams a strong default for mobile alerts, but the hard work sits above the SDK. Good React Native push notifications depend on alert rules, payload design, and user trust.

If your backend tracks state cleanly, sends fewer but better alerts, and respects privacy, the mobile layer stays simple. That's the kind of system people keep turned on.