technical-implementation

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 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 prerequisite with the Expo-compatible command:

npx expo install expo-notifications

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:

  • configure stores the Pushlane tenant, publishable key and ingest URL;
  • identify attaches subsequent events and device registration to a user;
  • track sends product events over HTTPS;
  • setAttributes stores current user values for segmentation and notification variables;
  • registerForPush asks permission and registers the raw APNs or FCM token;
  • setMarketingConsent records an explicit preference change;
  • reset detaches the current user on logout.

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. After authentication resolves, identify the user before tracking or registering the device.

import { Pushlane } from './lib/pushlane';

Pushlane.configure({
  tenantId: 'YOUR_TENANT_ID',
  publishableKey: 'lpk_live_YOUR_KEY',
});

Pushlane.identify(currentUser.id);
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.

Call Pushlane.reset() on logout before another account can take over the device. Then call identify() again after the next login. A single phone can represent different accounts over time, and lifecycle messages must follow the account rather than the hardware.

Share one stable ID with RevenueCat

RevenueCat's App User ID must equal the string passed to Pushlane.identify(). If your app already has a non-guessable backend user ID, use it 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.

Anonymous RevenueCat identities require more care because login and restore operations can create aliases or merge customers. The Pushlane worker resolves RevenueCat identity history, but the routing key still works best when the current RevenueCat App User ID and Pushlane external ID stay in lockstep. 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 after identity is known. 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:

  1. Use RevenueCat's dashboard test button to prove URL and secret authentication.
  2. Make a real sandbox purchase with the identified Expo test account 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 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:

  1. The app calls configure and identify with the expected ID.
  2. The permission request appears in the intended product context.
  3. Pushlane's Audience shows an APNs device for that user.
  4. A Pushlane test push reaches the device.
  5. A RevenueCat sandbox event appears for the same external ID.
  6. The event enters the intended flow once.
  7. A stop event during a delay cancels any pending message.
  8. The tap destination opens correctly from a terminated app.
  9. 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 account, 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.

Pushlane: Expo Push Notifications with RevenueCat