◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
Articles/App Dev
◇ App Dev/2026-10-09Beginner

When only notifications fail in Expo Go: three checks, then the shortest move to a development build

If push notifications never reach your Android device in Expo Go, check these three things before touching device settings, then move to a development build with the fewest steps. Also: how to spot what else Expo Go can't test.

Expo Go3Push notificationsDevelopment buildEAS Build20Troubleshooting41

I was trying out a notification message on a real Android phone, with Expo Go open, late one evening. The sending tool said "sent." The phone stayed silent.

I suspected the device first. I re-checked the permission, turned off battery saver, and switched from Wi-Fi to mobile data. Nothing arrived. About an hour had gone by before I stopped.

The cause was not the device, and not how I was sending. Expo Go itself no longer receives remote push on Android. When something that worked yesterday fails today, I now look at the tool's rules before I look at my own setup. That evening is where I settled on that order.

If you're stuck at the same spot, here is the order I'd check things in, and the smallest path to a development build.

The short version: remote push is out of scope for Expo Go on Android

Expo's documentation says that from SDK 53, remote push notifications are not available in Expo Go on Android. If you need to test them properly, the official route is a development build.

I haven't verified the iOS side of Expo Go myself, so please defer to the latest official FAQ there. It's the kind of behavior that can shift between versions.

What matters is that "it doesn't arrive" can come from three different causes.

SymptomWhere to lookHow to tell
The sender reports success, nothing appears on the deviceRuntime (is it Expo Go?)Show the execution environment inside the app
Token retrieval throwsprojectId, permission, Android credentialsRead the exception message
Works in a development build, not in productionPer-build credentials (FCM / APNs)Check the target build in EAS credentials

Only the first row is specific to Expo Go. The other two will still be waiting after you switch, so it's worth separating them now.

Check 1: let the code tell you whether this is Expo Go

Judging by how the phone looks invites assumptions. Ask the app instead.

// ExecutionEnvironmentBadge.tsx
// What it solves: confirm on screen whether you're running in Expo Go
import Constants, { ExecutionEnvironment } from 'expo-constants';
import { Text, View } from 'react-native';
 
export function ExecutionEnvironmentBadge() {
  // StoreClient means Expo Go (distinct from bare / standalone)
  const isExpoGo =
    Constants.executionEnvironment === ExecutionEnvironment.StoreClient;
 
  return (
    <View style={{ padding: 8 }}>
      <Text>
        Runtime: {isExpoGo ? 'Expo Go' : 'development build / production build'}
      </Text>
    </View>
  );
}

I use the enum rather than older checks like Constants.appOwnership, because older checks tend to disappear in later versions. The enum puts the basis of the decision in an official type.

If the screen says "Expo Go," stop hunting through notification settings. The tool is what needs to change.

Check 2: surface token errors instead of swallowing them

Even in a development build, the token can fail. The function tells you why, in words, but only if you let it.

// registerForPush.ts
// What it solves: check permission, projectId and the Android channel in one place
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
 
export async function registerForPush(): Promise<string> {
  if (Platform.OS === 'android') {
    // On Android 8+, no channel means no visible notification
    await Notifications.setNotificationChannelAsync('default', {
      name: 'default',
      importance: Notifications.AndroidImportance.DEFAULT,
    });
  }
 
  const current = await Notifications.getPermissionsAsync();
  let status = current.status;
  if (status !== 'granted') {
    status = (await Notifications.requestPermissionsAsync()).status;
  }
  if (status !== 'granted') {
    throw new Error('Notification permission was not granted (enable it in device settings)');
  }
 
  const projectId =
    Constants.expoConfig?.extra?.eas?.projectId ??
    Constants.easConfig?.projectId;
  if (!projectId) {
    throw new Error('projectId not found (did you run eas init?)');
  }
 
  const { data } = await Notifications.getExpoPushTokenAsync({ projectId });
  return data; // returned as ExponentPushToken[...]
}

On the calling side, catch the error and render the message on screen. If it only goes to a log, you end up looking back and forth between the phone and the terminal, and that's where most of the time disappears.

On Android, getting a token can succeed while delivery still needs FCM credentials. Those are set up through EAS credentials.

Check 3: probe the device path with a local notification

Remote push travels a long road: Expo's servers, then FCM or APNs. A local notification, scheduled on the device, skips that road entirely.

// scheduleLocalProbe.ts
// What it solves: confirm permission and channel are alive, without any server
import * as Notifications from 'expo-notifications';
 
export async function scheduleLocalProbe() {
  await Notifications.scheduleNotificationAsync({
    content: { title: 'Notification probe', body: 'If this appears in 5s, the device side is healthy' },
    trigger: {
      type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
      seconds: 5,
    },
  });
}

If it shows up after five seconds, permission and channel are fine, and you can narrow suspicion to "from the server onward." If it doesn't, permission or channel comes first.

Check the nearest thing first. Device, then app, then server. Keeping that order alone cut my wandering time noticeably.

The shortest move to a development build

Once you've confirmed Expo Go is the cause, move. After the first build, daily work feels close to Expo Go.

# 1) Install the dev client
npx expo install expo-dev-client
 
# 2) Log in to EAS and link the project (if not done yet)
npx eas-cli login
npx eas-cli init
 
# 3) Build a development build (Android device example)
npx eas-cli build --profile development --platform android

Add a development profile to eas.json:

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "android": { "buildType": "apk" }
    },
    "production": {}
  }
}

distribution: "internal" with buildType: "apk" lets you install straight onto a device without going through a store. When the build finishes, you get a QR code and a link; open it on the phone and install.

Starting the dev server differs only slightly from before:

# Start the dev server in a way that connects to the development build
npx expo start --dev-client

Notifications aren't the only thing Expo Go can't test

If notifications tripped you once, expect the same wall elsewhere. Adding a native module that isn't bundled with Expo Go, or wanting to verify app-specific native settings such as URL schemes or permission strings, also calls for a development build.

As an indie developer keeping several apps going, I find it cheaper to decide up front whether a feature fits in Expo Go. On any day I'm touching notifications, purchases, or custom native configuration, I start in a development build from the beginning.

And if Expo Go stops opening your preview because of sign-in, a different set of checks applies; I've written those up in Two logins to check first when Expo Go stops opening your preview.

One next step

One small thing is enough for today. Add ExecutionEnvironmentBadge as a single line on your app's settings screen. If it says "Expo Go," stop searching notification settings and start with npx expo install expo-dev-client.

Deciding the order of suspicion before a notification fails to arrive is a habit I intend to keep.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

◇ App Dev2026-09-02
Leave image out of eas.json and an SDK bump moves your Xcode and pnpm too
If eas.json has no image field, your builds run on the auto alias. Move one SDK alias and Xcode, Node.js, pnpm and fastlane all change together. Here is how to read the image name out of your last green build and decide whether to pin it before Xcode 27 lands.
⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
◇ App Dev2026-09-29
Your Expo Router App Flashes the Sign-In Screen on Every Launch. Rebuild the Guard With a Third State: Not Known Yet
Signed in, yet the sign-in screen flashes at launch; signed out, yet the screen never changes. How I rebuilt Expo Router Protected routes around three Supabase auth states, including the one the docs never name, and where the _layout.tsx files have to live.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links