Expo Push Notifications in 2026: Complete Gids met EAS, FCM en APNs
Volledige gids voor Expo Push Notifications in 2026: EAS-credentials voor APNs en FCM v1, hooks voor handlers, server-side verzending met expo-server-sdk en debugging-tips uit de praktijk.
Expo Push Notifications werken in 2026 via een tweelaagsysteem: je app registreert zich bij expo-notifications, ontvangt een ExpoPushToken, en de Expo Push Service stuurt berichten via FCM voor Android en APNs voor iOS. Sinds Expo SDK 55 heb je een EAS Build nodig voor productie (Expo Go doet het niet meer), worden credentials via EAS beheerd, en ondersteunt de nieuwe architectuur achtergrondafleveringen zonder bridge-overhead. Hieronder loop ik de volledige setup met je door, van permissies tot server-side verzending.
Push notifications werken sinds Expo SDK 53+ niet meer in Expo Go. Je hebt een development build of EAS Build nodig om te testen.
De ExpoPushToken is een abstractie boven FCM v1 (Android) en APNs (iOS); je server praat met één endpoint in plaats van twee.
Voor Android moet je een Firebase-project aanmaken en het google-services.json-bestand via EAS Credentials uploaden.
iOS vereist een APNs key (.p8) of certificate; EAS kan deze automatisch genereren als je App Store Connect-toegang verleent.
Notificatie-handlers worden in 2026 met hooks geschreven (het useNotificationObserver-patroon), niet meer met klasse-listeners.
Deep linking vanuit notificaties werkt soepel samen met Expo Router v7's typed routes.
Hoe werken Expo Push Notifications in 2026?
Als je vanuit React op het web komt, ken je de Push API en service workers. Daar registreert de browser zelf bij FCM of Mozilla's push-service, en jij krijgt een endpoint terug dat je server aanroept. In React Native gebeurt iets vergelijkbaars, maar omdat er geen browser is, doet expo-notifications de registratie tegen de native services: FCM voor Android, APNs voor iOS. Het verschil dat web-ontwikkelaars meestal verrast? Een mobiele app krijgt een token per installatie, niet per browser-sessie, en die token kan stabiel blijven gedurende de hele levensduur van de installatie.
De ExpoPushToken die je app ontvangt, ziet eruit als ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]. Dit is geen FCM- of APNs-token rechtstreeks, maar een wrapper. Wanneer je server een bericht naar https://exp.host/--/api/v2/push/send stuurt, mapt Expo's push-service de ExpoPushToken op de juiste native token en stuurt het bericht door via het bijbehorende kanaal. Sinds 2024 gebruikt Expo uitsluitend de FCM v1 HTTP API. De legacy API werd door Google in juni 2024 stopgezet, en alle apps die nog op de oude endpoint draaiden kregen 404's terug. (Ik heb dat zelf live meegemaakt op een retail-app, en het was geen leuk weekend.)
Wat verandert er in 2026? Met de Nieuwe Architectuur standaard ingeschakeld in Expo SDK 55 draait de notificatie-listener nu bridgeless. Concreet: callbacks die op een notification reageren, lopen niet meer via de oude JS-bridge maar direct over JSI. Voor jou als ontwikkelaar betekent dat foreground-handlers minder latentie hebben, en dat je rustig zware logic kunt uitvoeren in handleNotification zonder dat de UI hapert.
expo-notifications installeren en permissies vragen
Start met een schone Expo-app op SDK 55 of hoger. Push notifications werken sinds SDK 53 niet meer in Expo Go, dus je hebt een development build nodig. Dat klinkt zwaarder dan het is, één commando regelt het:
npx expo install expo-notifications expo-device expo-constants
npx eas build --profile development --platform all
De drie packages werken samen: expo-notifications doet de registratie en aflevering, expo-device vertelt of je op een echt apparaat draait (push werkt niet in de iOS simulator), en expo-constants geeft je het EAS project-id dat je nodig hebt om een ExpoPushToken te claimen.
Permissies vraag je op de eerste run die zinvol is, dus niet bij app-start. Op iOS toont Apple's prompt slechts één keer; weiger de gebruiker hem, dan moet je naar Instellingen verwijzen. Hier is de hook die ik in elk project gebruik:
import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import Constants from 'expo-constants';
import { useEffect, useState } from 'react';
import { Platform } from 'react-native';
export function useRegisterForPushNotifications() {
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
async function register() {
if (!Device.isDevice) {
setError(new Error('Push notifications vereisen een fysiek toestel'));
return;
}
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Standaard',
importance: Notifications.AndroidImportance.DEFAULT,
vibrationPattern: [0, 250, 250, 250],
});
}
const { status: existing } = await Notifications.getPermissionsAsync();
let finalStatus = existing;
if (existing !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
setError(new Error('Permissie geweigerd'));
return;
}
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
const pushToken = await Notifications.getExpoPushTokenAsync({ projectId });
setToken(pushToken.data);
}
register();
}, []);
return { token, error };
}
Twee details die makkelijk fout gaan. Android vereist een notification channel vóór je een notificatie kunt tonen (niet vóór registratie per se, maar in praktijk zet je het samen op). En de projectId moet expliciet meegegeven worden sinds SDK 49; eerder werd hij geraden uit je app slug, wat in monorepos vaak misging.
FCM v1 configureren voor Android
Sinds de legacy FCM API in juni 2024 werd uitgeschakeld, moet je Android-configuratie via FCM v1 lopen. Maak een Firebase-project aan (geen Google Cloud Console rechtstreeks, want Firebase wraps het), voeg een Android-app toe met dezelfde package name als in je app.json, en download het google-services.json-bestand.
Plaats het bestand niet in je repo. Upload het in plaats daarvan naar EAS:
eas credentials
Kies Android, dan Google Service Account Key, en vervolgens "Set up a Google Service Account Key for Push Notifications (FCM v1)." De wizard vraagt om de JSON service account key. Die maak je in Firebase Console onder Project settings, dan Service accounts, dan Generate new private key.
In app.json verwijs je naar het google-services-bestand:
Voor iOS gebruik je APNs. Apple ondersteunt twee authenticatie-methodes: certificaten (.p12) en authentication keys (.p8). Gebruik de p8 key. Apple beveelt hem aan, hij verloopt nooit, en één key werkt voor al je apps onder hetzelfde team. EAS regelt dit voor je als je tijdens eas credentials kiest voor "Let Expo handle the process."
Doe je het handmatig, dan ga je naar Apple's User Notifications-documentatie via Certificates, Identifiers & Profiles, dan Keys, dan "+", en vink Apple Push Notifications service aan. Download de .p8 en noteer het Key ID en je Team ID. EAS vraagt deze drie waarden tijdens credential-setup.
Belangrijk voor iOS in 2026: zorg dat je app.json de aps-environment-entitlement bevat. Expo voegt deze automatisch toe als je expo-notifications als plugin in app.json hebt staan:
Zodra je app de ExpoPushToken heeft, moet hij naar je backend. Drie dingen om aan te denken: koppel de token aan een user-id (niet alleen aan een sessie), sla op wanneer hij voor het laatst is gezien (stale tokens moet je periodiek opruimen), en sla device-info op zodat je per platform kunt filteren.
Een minimale Postgres-tabel:
CREATE TABLE push_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
platform TEXT NOT NULL CHECK (platform IN ('ios','android')),
app_version TEXT,
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX push_tokens_user_idx ON push_tokens(user_id);
Aan de client-kant stuur je de token bij login en bij elke app-start die login al heeft. Doe dit niet bij elke render. Gebruik een ref of een mutation die slechts triggert wanneer de token of user-id verandert. Voor projecten waar ik Zustand en TanStack Query combineer, hou ik de token in Zustand en sync hem in een TanStack mutation; dat geeft me retry-on-fail gratis.
Notificaties versturen vanaf de backend
De Expo Push API accepteert HTTP POST op https://exp.host/--/api/v2/push/send met een batch van maximaal 100 berichten. Een minimale Node-implementatie:
De useFcmV1: true flag is in expo-server-sdk 3.10+ standaard, maar zet hem expliciet om verwarring te voorkomen als je een ouder SDK-pinnen hebt. Het officiele Expo verzend-overzicht beschrijft de volledige ticket- en receipt-flow.
Wat veel teams missen: een verzonden bericht geeft een ticket terug, geen bevestiging van aflevering. Pas na ongeveer 30 minuten kun je via getPushNotificationReceiptsAsync ophalen of de notificatie door FCM of APNs is geaccepteerd. Tokens die "DeviceNotRegistered" teruggeven, moet je uit je database verwijderen. Anders bouw je een groeiende lijst dode tokens op die je rate-limit opvreet, en dat is precies de bug die ik in mijn vorige project pas na drie weken ontdekte.
Notificatie-handlers en deep linking
Aan de client-kant zijn er drie momenten waarop je code reageert: de app draait op de voorgrond, de app draait op de achtergrond, of de app is gesloten en de gebruiker tikt op de notificatie om hem te openen. In 2026 schrijven we deze handlers hooks-first, vergelijkbaar met hoe je useEffect op een visibility-change zou koppelen op het web.
import * as Notifications from 'expo-notifications';
import { useRouter } from 'expo-router';
import { useEffect } from 'react';
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});
export function useNotificationObserver() {
const router = useRouter();
const lastResponse = Notifications.useLastNotificationResponse();
useEffect(() => {
const data = lastResponse?.notification.request.content.data;
if (data?.url && typeof data.url === 'string') {
router.push(data.url);
}
}, [lastResponse]);
}
De useLastNotificationResponse-hook (toegevoegd in expo-notifications 0.27) is mijn favoriete API uit deze package. Hij rapporteert ook notificaties die de app vanuit een cold start hebben geopend, iets waar je voorheen met getLastNotificationResponseAsync handmatig om de race condition heen moest dansen.
Combineer dit met Expo Router v7's deep linking: stop in je notification payload een veld data.url met een route-pad (bijvoorbeeld /order/42), en de observer stuurt de gebruiker direct naar dat scherm. Geen aparte URL-parsing nodig.
Foreground vs background gedrag
Op iOS toont het systeem foreground-notificaties standaard niet. Je moet expliciet shouldShowBanner: true teruggeven uit handleNotification. Op Android worden ze sowieso getoond, tenzij de notification channel op IMPORTANCE_NONE staat. Voor stille (data-only) notificaties zet je beide booleans op false en gebruik je de data om bijvoorbeeld een TanStack Query-cache te invalideren.
Testen zonder naar productie te pushen
Expo biedt een push notification tool in het dashboard waar je een ExpoPushToken plakt en direct een test-bericht stuurt. Voor CI of automatische tests gebruik je dezelfde HTTP POST naar exp.host, met testtokens uit een fixture-account.
Een patroon dat me veel debugtijd heeft bespaard: log de notificatie aan beide kanten. Aan je backend log je het ExpoPushTicket; aan de client-kant log je elke binnenkomende notificatie naar je crash-reporting-tool. Wanneer een gebruiker meldt "ik krijg geen meldingen", check je eerst of je server een ticket heeft, dan of er een receipt is, en pas dan kijk je naar de client. Voor diepere inspectie van de notificatiestroom is de React Native DevTools network-tab in 2026 essentieel. Je ziet daar de hele payload heen en weer gaan zonder Charles Proxy te hoeven opstarten.
Veelgemaakte fouten en debugging
Vijf fouten die ik telkens opnieuw zie in code reviews:
Token vragen vóór permissie. getExpoPushTokenAsync faalt stilletjes op iOS zonder granted permission. Je krijgt geen exception, alleen een leeg resultaat.
Geen channel op Android. Op Android 13+ verschijnt zonder channel geen notificatie. De default channel maakt expo-notifications zelf, maar voor multi-channel apps (transactioneel, marketing, etc.) moet je ze expliciet aanmaken.
Vergeten te re-registreren na backup-restore. Als een gebruiker zijn iPhone restoret vanuit iCloud, krijgt de nieuwe installatie een nieuwe APNs-token. Controleer bij app-start of je opgeslagen token nog matcht.
EAS Build cache. Verander je iets aan je app.json push-config, dan moet je een nieuwe build draaien — een OTA-update is niet voldoende voor native config-wijzigingen.
Verkeerd bundle ID in APNs key. Apple staat één APNs key per team toe, niet per app. Werk je in meerdere apps onder hetzelfde team, vergeet dan niet dat je de key in EAS opnieuw moet toewijzen aan elk EAS-project.
Veelgestelde vragen
Werken Expo Push Notifications nog in Expo Go in 2026?
Nee. Sinds SDK 53 is push notification-functionaliteit uit Expo Go verwijderd. Je hebt een development build of een productie EAS Build nodig om push te testen of te gebruiken. Dit was een bewuste keuze van het Expo-team omdat de gedeelde Expo Go bundle ID te veel ruis veroorzaakte in APNs/FCM-quota.
Wat is het verschil tussen een ExpoPushToken en een FCM-token?
Een ExpoPushToken is een wrapper die Expo's push-service mapt op de juiste native token. Je server praat met één Expo-endpoint en Expo routeert het bericht naar FCM (Android) of APNs (iOS). Wil je rechtstreeks FCM-tokens, dan kan dat via getDevicePushTokenAsync, maar je verliest dan de cross-platform abstractie.
Kan ik push notifications versturen zonder EAS?
Versturen kan iedereen, want de Expo Push API accepteert HTTP requests vanaf elke server. EAS heb je nodig voor de build-kant: het uploaden van credentials (APNs key, FCM service account) en het genereren van native binaries met de juiste entitlements. Een bare React Native-app zonder EAS kan ook, maar dan beheer je APNs en FCM rechtstreeks zonder Expo's wrapper.
Hoe lang blijft een ExpoPushToken geldig?
De token blijft geldig zolang de app-installatie bestaat. Een token wordt ongeldig bij app-verwijdering, een iCloud/Google-backup-restore, of als de gebruiker notificaties intrekt en opnieuw toestaat. Verwijder tokens uit je database zodra een ticket "DeviceNotRegistered" terugmeldt.
Werken stille (silent) push notificaties op iOS en Android?
Ja, maar met beperkingen. Op iOS gebruik je _contentAvailable: true in de payload; het systeem mag de aflevering vertragen of overslaan als de batterij laag is. Op Android stuur je een data-only notification (zonder notification-object); deze worden betrouwbaarder afgeleverd, maar wakeup-restricties op Android 13+ kunnen meespelen.
Leer alles over Expo Router v7 in deze praktische Nederlandse gids. Van file-based routing en Stack.Protected authenticatie tot modale schermen, type-veilige navigatie en automatische deep linking in Expo SDK 55. Met werkende codevoorbeelden.
Een compleet overzicht van Expo SDK 55 met React Native 0.83, React 19.2, Hermes v1, het definitieve einde van de Legacy Architecture, nieuwe navigatie-API's en een stapsgewijze migratiehandleiding.