DEV Community

Chris F A
Chris F A

Posted on Originally published at applighter.hashnode.dev

What React Native Devs Actually Struggle With: A Year of Support Tickets

TL;DR

  • We categorized twelve months of Applighter support tickets. The framework isn't the problem; the perimeter is.
  • Top 3: EAS Build failing on first run, Supabase RLS returning [], env vars undefined in production builds.
  • Run bunx expo doctor before asking for help. It usually names the broken file.
  • Only EXPO_PUBLIC_ vars reach the app bundle, and your .env is probably gitignored from EAS.
  • Use expo/fetch for AI streaming. The default RN fetch won't stream.
  • Put your cache reset in a script. You'll type it wrong at 2am otherwise.

People file tickets when they're bleeding, not when they're reflecting. So a year of support tickets paints a very different picture than community surveys. Here's the top ten, ordered by volume, with the fix for each.

# Problem Layer
1 EAS Build fails on first run build
2 Supabase RLS returns empty arrays after auth backend
3 Env vars don't reach the native binary build
4 Metro cache poisoning after a dependency bump build
5 Missing iOS Info.plist permission strings ios
6 Android package name conflicts on rename android
7 Android push silently not delivering native
8 AI streaming stalls / drops tokens ai
9 Stripe webhook not reaching a Supabase Edge Function payment
10 Voice transcription latency on mid-range Android ai

1. EAS Build fails on first run

Clone, prebuild, build, and it dies at "Install pods" or in Fastlane signing. Three usual causes:

  • Template SDK and local CLI disagree (and nobody ran expo doctor)
  • A device profile used before provisioning exists
  • No EXPO_TOKEN in CI

Our templates now ship a preflight that blocks the build if doctor is red:

#!/usr/bin/env bash
# scripts/preflight.sh
set -e
bunx expo doctor || { echo "❌ Fix expo doctor issues before building"; exit 1; }
Enter fullscreen mode Exit fullscreen mode

If you're building from CI, make sure the token is set:

env:
  EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

2. Supabase RLS returns [] after auth

"I'm signed in, I see my user ID, but select * returns nothing." Supabase isn't broken; RLS is working.

A typical owner-only policy:

alter table projects enable row level security;

create policy "owners can read their projects"
on projects for select
using (auth.uid() = owner_id);
Enter fullscreen mode Exit fullscreen mode

Checklist:

  • Log the raw Authorization header. The token is often undefined.
  • auth.uid() vs auth.jwt() ->> 'sub' behave differently with anon requests mixed in.
  • Never ship the service_role key in a client bundle.
  • Access tokens expire after an hour by default. Make sure the session refreshes:
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from '@react-native-async-storage/async-storage';

export const supabase = createClient(
  process.env.EXPO_PUBLIC_SUPABASE_URL!,
  process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      storage: AsyncStorage,
      autoRefreshToken: true,
      persistSession: true,
      detectSessionInUrl: false,
    },
  },
);
Enter fullscreen mode Exit fullscreen mode

The full auth setup (email, OAuth, Sign in with Apple) is in our Supabase auth in React Native guide.

Mental model: policies are WHERE clauses that always run.

3. Env vars are undefined in production

Works in Expo Go, works in the simulator, undefined in the standalone build. Two reasons: .env is usually gitignored so EAS never sees it, and only EXPO_PUBLIC_ vars are inlined into the bundle.

- API_URL=https://api.example.com
+ EXPO_PUBLIC_API_URL=https://api.example.com
Enter fullscreen mode Exit fullscreen mode

Then declare the values for EAS, either as EAS environment variables or per profile:

{
  "build": {
    "production": {
      "env": { "EXPO_PUBLIC_API_URL": "https://api.example.com" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Reminder: EXPO_PUBLIC_ means public. No secrets.

4. Metro cache poisoning

Dependency bump, inscrutable crash, nothing works until someone says "reset the cache." Script it:

{
  "scripts": {
    "reset": "watchman watch-del-all; rm -rf node_modules ios/Pods .expo && bun install && bunx expo prebuild --clean"
  }
}
Enter fullscreen mode Exit fullscreen mode

Lighter first attempt: bunx expo start --clear.

5. Missing Info.plist permission strings

Camera, mic, photos, or location without a purpose string means a failed upload or a review rejection. Say why, specifically:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "NSCameraUsageDescription": "This app needs camera access to scan documents.",
        "NSMicrophoneUsageDescription": "This app records voice notes."
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

"This app uses the camera" isn't enough.

6. Android package rename breaks the build

The package name lives in app.json, build.gradle, AndroidManifest.xml, the Java/Kotlin source path, google-services.json, and OAuth redirect URIs. Miss one and you get a ClassNotFoundException on launch. On managed Expo, let the tooling regenerate:

rm -rf android
# set expo.android.package in app.json
bunx expo prebuild --clean
Enter fullscreen mode Exit fullscreen mode

7. Android push says "ok" but nothing shows

Usual suspects: FCM credentials not configured in EAS, OEM battery optimization, no channel, or no Android 13+ permission. Cover the last two in code:

import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';

export async function setupAndroidPush() {
  if (Platform.OS !== 'android') return;
  await Notifications.setNotificationChannelAsync('default', {
    name: 'Default',
    importance: Notifications.AndroidImportance.HIGH,
  });
  const { status } = await Notifications.requestPermissionsAsync();
  if (status !== 'granted') console.warn('Push permission denied');
}
Enter fullscreen mode Exit fullscreen mode

8. AI streaming stalls or drops tokens

React Native's default fetch doesn't do ReadableStream. Use expo/fetch:

import { fetch } from 'expo/fetch';

const res = await fetch(url, { method: 'POST', body, headers });
const reader = res.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  onChunk(decoder.decode(value, { stream: true }));
}
Enter fullscreen mode Exit fullscreen mode

9. Stripe webhook never reaches Supabase

Three causes: signing secret mismatch, JWT verification on (Stripe doesn't send a Supabase JWT), or pointing at a preview URL.

# supabase/config.toml
[functions.stripe-webhook]
verify_jwt = false
Enter fullscreen mode Exit fullscreen mode

Then fire a test event before going live:

stripe trigger checkout.session.completed
Enter fullscreen mode Exit fullscreen mode

10. Voice transcription feels slow on budget Android

Mostly hardware and network, not a bug. Chunk audio at shorter intervals (a few seconds, not ten), show a typing indicator, and fall back to a smaller model tier on older devices:

const model = isHighEndDevice ? config.fastModel : config.smallModel;
Enter fullscreen mode Exit fullscreen mode

The takeaway

React Native the framework has stabilized. The pain is in the seams: build tooling, permissions, backends, push, AI providers, device quirks. If you're prioritizing what to learn this week:

  1. eas.json and build profiles
  2. Your backend's auth model (RLS if Supabase)
  3. Env var propagation
  4. A scripted cache reset
  5. Apple's common rejection reasons

Which of these ten has cost you the most hours? Or is your worst one not on the list? Drop it in the comments. 👇

Top comments (0)