Requesting Permissions in Expo: the Short Answer
Requesting a permission in an Expo app is four steps: install the module that owns that capability, declare a purpose string that explains why you need it, call the module's requestPermissionsAsync() method when the user reaches the feature, and branch on the status the OS returns. Expo wraps the native iOS and Android permission APIs behind one consistent JavaScript interface, so the same pattern - check current status, request if needed, handle granted or denied - works for the camera, the photo library, location, notifications, and the microphone.
The part that trips up first-time builders is not the code. It is the purpose string and the timing. iOS refuses to show a permission prompt at all if the matching purpose string is missing, and App Review rejects apps that request a permission the app never actually uses, or that ask for everything on launch before the user has done anything. Get the purpose strings right and request at the point of need, and permissions stop being a rejection risk.
The Permission Modules You Actually Need
In Expo, permissions live inside the module that provides the capability, not in one central permissions package. You install the module for the feature, and that module exposes the request and status methods. This means you only ship the native permission code for capabilities your app genuinely uses, which is exactly what App Review wants to see.
Common Expo permission modules and their request methods
| Capability | Module | Request method |
|---|---|---|
| Camera | expo-camera | Camera.requestCameraPermissionsAsync() |
| Photo library | expo-image-picker | ImagePicker.requestMediaLibraryPermissionsAsync() |
| Location | expo-location | Location.requestForegroundPermissionsAsync() |
| Push notifications | expo-notifications | Notifications.requestPermissionsAsync() |
| Microphone | expo-camera / expo-av | requestMicrophonePermissionsAsync() |
| Contacts | expo-contacts | Contacts.requestPermissionsAsync() |
Install a module the Expo way so its config plugin is registered: run npx expo install expo-camera (or the module you need) rather than a plain npm install. The expo install command pins a version compatible with your SDK and wires the module's config plugin into your build, which is what adds the native permission entries during EAS Build. Skipping this is a frequent cause of a permission that works in Expo Go but fails in a production build.
Write a Purpose String for Every Permission
A purpose string is the sentence the OS shows the user in the permission dialog. On iOS it is mandatory: if you request a permission whose purpose string is missing from your app configuration, the app crashes or the prompt silently fails, and App Review rejects the build under the privacy guidelines. On Android the equivalent is the permission declaration in the manifest, which Expo's config plugins add for you when you install the module.
- For a managed Expo app, most modules let you set the purpose string through the config plugin in app.json. For example, expo-camera accepts a cameraPermission string in its plugin options that becomes the iOS NSCameraUsageDescription.
- For permissions not covered by a plugin option, set the raw iOS key directly under ios.infoPlist in app.json - for example NSLocationWhenInUseUsageDescription for foreground location or NSPhotoLibraryUsageDescription for the photo library.
- Write each string as a specific, honest reason: what the app does with the access and what the user gets. Avoid generic 'for app functionality' text - App Review flags it, and users are more likely to deny.
- Rebuild after changing purpose strings. They are baked into the native app at build time, so a change in app.json only takes effect on the next EAS Build, not on a JavaScript reload.
- Only declare purpose strings for permissions you actually request in code. A declared permission the app never uses is itself a rejection reason under the guideline that says apps must only request access to data relevant to their function.
Request at the Point of Need, Not on Launch
Request each permission at the exact moment the user does something that requires it. When someone taps a camera button, that is when you request camera access. When they tap 'find places near me', that is when you request location. Front-loading every permission on the splash screen is the single biggest reason users deny - they have no context for why the app wants their location before they have even seen it - and App Review treats a wall of launch-time prompts as a red flag.
- Check the current status first with getPermissionsAsync() (or the module's equivalent). If it is already granted, use the feature immediately without prompting again.
- If the status is undetermined, call requestPermissionsAsync(). This shows the system dialog once. The user's answer comes back as a status of granted or denied.
- If granted, proceed to open the camera, read the location, or schedule the notification.
- If denied, show your own in-app explanation of what the feature does and a button that deep-links to the app's settings page (Linking.openSettings()), because iOS will not show the system prompt a second time once the user has denied it.
- Never block the whole app on a denied permission unless the permission is core to the app's single purpose. A note-taking app should still open its notes if the user denies the camera used for one optional attach-photo feature.
iOS shows a permission's system dialog only once. After the first denial, calling requestPermissionsAsync() again returns denied without showing anything. That is why the fallback for a denied permission is always your own screen plus a link to Settings, never a second silent request that appears to do nothing.
Handle Location's Extra Layer
Location is the one permission with more than a simple yes or no. Both iOS and Android split location into foreground (only while the app is open) and background (also when the app is closed), and modern iOS lets the user grant approximate location instead of precise. Request the least you need: most apps only need foreground location, and asking for background location you do not use is a strong rejection trigger.
- For a feature that only needs location while the user is looking at the app, call Location.requestForegroundPermissionsAsync() and stop there. Do not request background location.
- Only request background location with requestBackgroundPermissionsAsync() if your app has a genuine background use, such as a run-tracking app that records a route with the screen off. You must justify it to App Review with a specific purpose string, and the review is stricter.
- Handle the approximate-location case: on iOS the user can grant reduced accuracy. Read the accuracy the OS returns and degrade gracefully rather than assuming you always have a precise coordinate.
- Set a clear NSLocationWhenInUseUsageDescription for foreground and, only if needed, NSLocationAlwaysAndWhenInUseUsageDescription for background. Never declare the background key unless you request background location.
Let Claude Code Write the Permission Flow
The permission pattern is mechanical and repetitive, which is exactly what Claude Code handles well from the desktop app. Describe the feature - 'a button that opens the camera to scan a receipt' - and it writes the full flow: the status check, the request, the granted path that opens the camera, and the denied path that shows an explanation with a link to Settings. It also knows which purpose string key each module needs and can add the app.json plugin entry for you.
- Tell Claude Code which capability you need and when the user reaches it. It picks the right module and request method rather than you memorizing six different API shapes.
- Ask it to add the purpose string to app.json - either as a config plugin option or a raw ios.infoPlist key - with a specific, review-safe sentence you can edit.
- Have it generate the denied-permission fallback (in-app explanation plus Linking.openSettings()), the piece first-timers most often forget and the reason a feature silently does nothing after a denial.
- Ask it to write a small reusable hook - something like usePermission(module) - so every feature in the app checks and requests permissions the same way instead of copy-pasting the logic per screen.
Common Permission Mistakes and How to Avoid Them
Permission bugs cluster around a handful of predictable mistakes. Most come from assuming a permission is granted, forgetting the purpose string, or requesting everything up front instead of at the point of need.
Common Expo permission mistakes
| Mistake | What goes wrong | Fix |
|---|---|---|
| Using a feature without checking the permission status | The app crashes or silently fails on devices where the user denied access | Always check status and request before using the capability; branch on granted vs denied |
| Missing purpose string for a requested permission | The iOS prompt fails or the app crashes; App Review rejects under the privacy guidelines | Add the matching purpose string via the config plugin or ios.infoPlist, then rebuild |
| Requesting every permission on the splash screen | Users deny with no context; App Review flags launch-time permission walls | Request each permission at the moment the user triggers the feature that needs it |
| Re-requesting a denied permission expecting the prompt to reappear | iOS never shows the system dialog a second time, so the request appears to do nothing | After a denial, show your own explanation and link to Settings with Linking.openSettings() |
| Declaring background location you do not use | Rejection for requesting access unrelated to the app's function | Only request foreground location unless you have a real, justified background use case |
TL;DR
Request permissions in your Expo app by installing the module for the capability (expo-camera, expo-image-picker, expo-location, expo-notifications), declaring a specific purpose string for every permission through the config plugin or ios.infoPlist, and calling requestPermissionsAsync() at the moment the user reaches the feature - not on launch. Check the status the OS returns: use the feature if granted, and if denied, show your own explanation with a link to Settings, because iOS only shows each system prompt once. Keep declared permissions and requested permissions in sync so App Review sees no unused access. Claude Code writes the whole flow; you write the purpose string, because only you know exactly what your app does with the access.
Short, practical drops on app ideas, validating fast, store listings, and passing app review. No spam, unsubscribe anytime.
Frequently asked questions
Why does my Expo app crash when I request camera or photo permissions?
Almost always a missing purpose string. iOS requires a usage description for every permission you request - for the camera that is NSCameraUsageDescription. If it is absent, the app crashes when it tries to show the prompt. Add the purpose string through the module's config plugin option in app.json (or under ios.infoPlist) and run a new EAS Build, since purpose strings are baked in at build time.
When should I ask the user for a permission?
At the moment they trigger the feature that needs it - when they tap the camera button, request camera access; when they tap 'near me', request location. Requesting on app launch gives the user no context, leads to more denials, and App Review treats launch-time permission walls as a red flag. Point-of-need requests convert better and read as trustworthy.
What happens if the user denies a permission?
The request returns a denied status, and on iOS the system dialog will not appear again on future requests. Your app should detect the denial and show its own in-app explanation of what the feature does, with a button that opens the app's settings page via Linking.openSettings() so the user can enable it manually. Do not silently re-request - nothing will happen.
Do I need EAS Build to test permissions, or does Expo Go work?
Expo Go includes many common permissions, so foreground features like the camera and photo library often work there for quick testing. But Expo Go uses its own bundled native config, so purpose strings and some permission behavior only reflect your real app in a development build or an EAS Build. Always verify permission behavior in a build made from your own config before you submit.
Can I request background location in an Expo app?
Yes, with Location.requestBackgroundPermissionsAsync(), but only request it if your app has a genuine background use such as recording a route with the screen off. Apple reviews background location strictly and rejects apps that request it without a clear, justified purpose string. If you only need location while the app is open, request foreground location alone.
How do I avoid a privacy rejection over permissions?
Keep declared permissions and requested permissions in sync - only declare a purpose string for a permission you actually request in code, and only request permissions your app genuinely uses. Write specific, honest purpose strings that state what you do with the access. Request at the point of need, not on launch. Those three habits remove the most common permission-related rejections.
Last reviewed by David on September 14, 2026


