Pushlane: Expo Push Notifications with RevenueCat
Add Pushlane to an Expo subscription app with its one-file drop-in, register the raw APNs token, share RevenueCat identity and verify a real iOS flow.
An Expo subscription app does not need a second notification package or a mandatory sign-up screen to connect product events and RevenueCat lifecycle events to Pushlane. Pushlane's Expo path is a TypeScript drop-in: one file that uses your existing expo-notifications installation and fetch.
The important constraint is platform delivery. On iOS, the drop-in registers the raw APNs token and Pushlane delivery is proven end to end. On Android, it registers the raw FCM token for audience visibility, but Pushlane does not currently deliver through FCM. This tutorial therefore builds and verifies the production path on iOS.
Use a development build, not Expo Go
Push notifications require native capabilities and credentials. The Pushlane Expo quickstart requires an EAS development build or a local prebuild workflow, a physical iOS device and expo-notifications configured for push.
Expo's official push notification setup guide covers the native project configuration. Expo also documents that its notification API can return the native device token through getDevicePushTokenAsync, which can be used with a provider other than the Expo push service. See the Expo push FAQ.
Install the push prerequisite with the Expo-compatible command. If the app can operate before sign-in, also install a persistent store so the drop-in can keep one anonymous identity across launches:
npx expo install expo-notifications
npx expo install expo-secure-store
The store is not cosmetic. Without persistent storage, Pushlane refuses to create a disposable anonymous ID on every cold start because that would fragment flows and inflate the audience. Apps that always call identify() with a real account ID are unaffected.
Do not install expo-pushlane. It is not a published npm package. Copy the maintained lib/pushlane.ts drop-in from Pushlane's Expo quickstart or ask Pushlane's MCP setup tool for the current source. Keeping that file aligned with the docs is part of the integration.
Understand what the drop-in does
The one-file client provides these operations:
configurestores the Pushlane tenant, publishable key and ingest URL;identifyoptionally moves events and device registration from the install identity to a signed-in user;tracksends product events over HTTPS;setAttributesstores current user values for segmentation and notification variables;registerForPushasks permission and registers the raw APNs or FCM token;setMarketingConsentrecords an explicit preference change;resetdetaches the current user on logout and mints a fresh anonymous install identity.
It emits app_open when you call start(). It does not automatically emit every notification received or opened event. Instrument only the product events you need and avoid double-counting app_open elsewhere.
The default ingest URL remains the Pushlane worker endpoint used by shipped clients. The public custom domain does not require you to rewrite a working drop-in.
Configure in the correct order
At app startup, configure the client, start event collection and register the device. None of those steps requires an account. With persistent storage available, the drop-in mints one anon_... ID per install and uses it for events, attributes and the push token.
import { Pushlane } from './lib/pushlane';
Pushlane.configure({
tenantId: 'YOUR_TENANT_ID',
publishableKey: 'lpk_live_YOUR_KEY',
});
Pushlane.start();
await Pushlane.registerForPush();
The lpk_... key is publishable and belongs in the app. It authenticates app ingest. It is not the secret used for RevenueCat's server webhook.
If authentication resolves later, call Pushlane.identify(currentUser.id). The drop-in re-registers the known device token under that account automatically. Call Pushlane.reset() on logout before another account can take over the device; the device then receives a fresh anonymous identity. A single phone can represent different accounts over time, and lifecycle messages must follow the current identity rather than the hardware.
Choose one identity path for RevenueCat
If your app has accounts, RevenueCat's App User ID should equal the string passed to Pushlane.identify(). Use the same non-guessable backend user ID for both systems.
const userId = currentUser.id;
Pushlane.identify(userId);
await Purchases.logIn(userId);
RevenueCat says custom App User IDs are case-sensitive, project-scoped and should not be emails or easily guessed values. Review the official RevenueCat identity guide before choosing the convention.
If the app has no accounts, configure RevenueCat first, read its current App User ID and pass that value to Pushlane.identify() on every launch. RevenueCat's anonymous $RCAnonymousID:... value is a valid Pushlane external ID. This keeps the device, product events and webhook on the same identity without inventing a sign-up requirement.
Do not let Pushlane mint one anonymous ID while RevenueCat independently mints another if RevenueCat webhooks must trigger that user's flows. The standalone Pushlane anonymous path is useful before billing is configured, but two unrelated IDs cannot be joined by guesswork. Login and restore operations can create RevenueCat aliases or merge customers; Pushlane resolves current, original and aliased RevenueCat identities, while matching the current ID in both SDKs remains the clearest path. The full model is in RevenueCat App User ID and device token matching.
Request notification permission in product context
The drop-in calls Notifications.getPermissionsAsync() and, when necessary, requestPermissionsAsync() for alert, badge and sound. Call registerForPush() only after your UI has explained a specific benefit and the person chooses to continue.
Examples include:
- enabling a reminder after a study time is selected;
- following a budget alert after an account is connected;
- receiving an update after a long-running export begins.
Do not request permission merely because the paywall closed. Apple's official notification permission guide recommends asking in context. If permission is denied, continue the product experience and offer a settings explanation later when a relevant feature needs notifications.
The current Expo drop-in requests standard authorization. It does not implement Apple's provisional quiet-notification flow. Do not describe its behavior as provisional.
Register the native token, not an Expo push token
expo-notifications supports two token types:
- an Expo push token for Expo's hosted sending service;
- the native device token returned by
getDevicePushTokenAsync.
Pushlane uses the second. On iOS, the token type is ios and the data is the raw APNs token. The drop-in normalizes that value and sends platform: "apns" to Pushlane. On Android, it sends the raw token as platform: "fcm".
This distinction matters because an ExponentPushToken[...] value cannot be sent directly to APNs. Do not replace the drop-in call with getExpoPushTokenAsync().
Apple advises requesting a current device token and forwarding it to the provider server because tokens can change. Re-run registration on launch. If a person signs in afterward, identify() rebinds the known token to that account. Pushlane deduplicates current registrations server-side.
Track product events, not billing claims
Use Pushlane.track for behavior your app directly observes:
Pushlane.track('paywall_viewed', {
placement: 'onboarding_complete',
offering: 'default',
});
Pushlane.track('first_workout_completed', {
duration_minutes: 18,
});
Do not track purchase_completed, trial_started or subscription_cancelled as authoritative client events when RevenueCat is connected. Subscription state should arrive from RevenueCat's server webhook. That prevents a stale or modified app client from claiming billing outcomes.
Call setAttributes for durable traits that change targeting or copy, and refresh them when the value changes:
Pushlane.setAttributes({
first_name: currentUser.firstName,
onboarding_goal: profile.goal,
locale: deviceLocale,
});
Do not send email addresses, access tokens or sensitive onboarding answers simply because they could personalize a message. Every variable should have a natural fallback in Pushlane.
Connect the RevenueCat webhook
In Pushlane Settings, generate the dedicated rcw_... webhook secret. In RevenueCat, create a webhook pointing at the URL Pushlane provides and set that secret as the Authorization value. Follow RevenueCat + Pushlane setup for the exact dashboard steps.
RevenueCat's official webhook guide recommends idempotent processing because events can be delivered more than once. Pushlane deduplicates connected events using RevenueCat's event ID.
Run two tests:
- Use RevenueCat's dashboard test button to prove URL and secret authentication.
- Make a real sandbox purchase with the same Expo test identity to prove user matching and lifecycle event mapping.
Dashboard TEST events are acknowledged but do not enter Pushlane flows. A green transport test is not a subscription-flow test.
Build one lifecycle flow
Choose an event you have observed in the Pushlane catalogue. A safe first flow is a paid purchase welcome on revenuecat.initial_purchase, with a branch that checks production environment and the period type before using paid-access copy.
revenuecat.initial_purchase
-> branch: production, paid period, reachable iOS device
-> send: one premium setup action
-> wait for first_premium_action_completed
-> exit on completion
Do not create a customer-facing flow that sends on sandbox events. Pushlane exposes is_production on the bridged event for that guard.
For a free trial, RevenueCat does not send a TRIAL_STARTED webhook type. It sends INITIAL_PURCHASE with period_type: "TRIAL"; Pushlane emits both revenuecat.initial_purchase and the derived revenuecat.trial_started flow event. A conversion arrives as RENEWAL with is_trial_conversion: true, which Pushlane also exposes as revenuecat.trial_converted. RevenueCat's event types reference is the source of truth for those raw fields.
For payment failure, cancellation and expiration, use the state-specific exits in RevenueCat push automation blueprints. A single sequence should not guess whether access remains active.
Verify on a physical iPhone
Use an EAS development build or production-like build on a physical device. Verify in this order:
- The app calls
configureand establishes the intended anonymous or account identity. - The permission request appears in the intended product context.
- Pushlane's Audience shows an APNs device for that user.
- A Pushlane test push reaches the device.
- A RevenueCat sandbox event appears for the same external ID.
- The event enters the intended flow once.
- A stop event during a delay cancels any pending message.
- The tap destination opens correctly from a terminated app.
- Logout detaches the device before a different account signs in.
If step 4 fails, debug APNs configuration before RevenueCat. If step 5 fails, debug the webhook and shared ID before editing notification copy.
Keep the Android boundary visible
The Expo drop-in registers FCM tokens on Android, so Android installations appear in the audience and can send product events. Pushlane's sender is currently APNs-only. An FCM-only user is suppressed with an explicit unsupported-delivery reason rather than silently counted as sent.
Do not launch an all-platform lifecycle campaign based on iOS proof. Keep delivery targeting and product reporting separated by platform until Android delivery is supported and verified end to end.
The integration is one file, but the proof is end to end
Copying lib/pushlane.ts is the smallest implementation step. The real integration is complete when the native APNs token belongs to the correct current identity, the RevenueCat webhook arrives for the same ID, a production guard protects customer delivery and the stop event cancels stale messages.
Start from the maintained Expo quickstart, then prove one sandbox lifecycle on one physical iPhone. That evidence is more valuable than a large flow diagram built before identity and delivery work.