Apple's privacy manifest rule went live on May 1, 2024, and Expo apps sit in the middle of it because the file lives in the iOS native folder, not in the JavaScript bundle. For a managed Expo project, the right path is the privacyManifests field in app.json, with prebuild generating the manifest at build time. For a bare Expo project, the file goes into Xcode by hand at the iOS target root. The trap that catches most developers is mixing both approaches in the same repo.
Short answer
For a managed Expo project, add the privacyManifests block under expo.ios in app.json and let prebuild generate PrivacyInfo.xcprivacy for you. For a bare Expo project, create the file by hand in Xcode at the iOS target root. Either way, declare the required reason API categories used by your code and your dependencies, the NSPrivacyTracking flag, and any NSPrivacyTrackingDomains your app contacts. Apple reads the app-target manifest before any pod-level manifests.
What you should know
- The manifest lives in the app target, not the JS bundle. Expo's prebuild step writes PrivacyInfo.xcprivacy into the ios directory from your app.json config.
- Five required reason API categories cover most apps. UserDefaults, FileTimestamp, SystemBootTime, DiskSpace, and ActiveKeyboard each carry their own short reason codes.
- Managed and bare workflows handle the file differently. Managed uses expo.ios.privacyManifests in app.json; bare keeps a manual file in Xcode. Keeping both creates drift.
- Apple emails the missing reason warning before the rejection. The warning arrives from App Store Connect after the upload finishes processing.
- Tracking domains are required only when NSPrivacyTracking is true. Listing them with tracking off flips the App Privacy survey into an inconsistent state.
Why does Apple require PrivacyInfo.xcprivacy at all?
The short answer is that Apple wants a machine-readable record of which APIs an app or SDK touches, so the App Store can compare what an app says it does with what is actually compiled into the binary. The rule grew out of WWDC 2023 and went into force on May 1, 2024, per Apple's announcement on upcoming third-party SDK requirements.
The rule has three moving parts. First, third-party SDKs on Apple's commonly used list must ship a signed PrivacyInfo.xcprivacy inside their distribution. Second, the host app must declare the same information in its own PrivacyInfo.xcprivacy, because Apple reads the app-target manifest as the source of truth. Third, every privacy-sensitive API call in the app or any SDK must list an approved reason code, taken from Apple's required reason API list.
Starting March 13, 2024, Apple began sending email warnings from App Store Connect when an upload was missing reasons. The hard cutover was May 1, 2024, and after that date a build with a missing reason is rejected at processing time, not at human review. The rejection language usually quotes ITMS-91056 and names the specific reason category that is unaccounted for.
What does the managed Expo workflow set in app.json?
The short answer is the privacyManifests object under expo.ios, which the prebuild step translates into a PrivacyInfo.xcprivacy file at the ios target root. The shape comes straight from Expo's privacy manifests guide and accepts the same four top-level keys Apple defines.
A minimal config for a managed app that reads from UserDefaults and queries file timestamps looks like this:
{
"expo": {
"ios": {
"privacyManifests": {
"NSPrivacyTracking": false,
"NSPrivacyTrackingDomains": [],
"NSPrivacyCollectedDataTypes": [],
"NSPrivacyAccessedAPITypes": [
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
"NSPrivacyAccessedAPITypeReasons": ["CA92.1"]
},
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp",
"NSPrivacyAccessedAPITypeReasons": ["C617.1"]
}
]
}
}
}
}
Config plugin support is part of @expo/config-plugins from SDK 50 onward, which means the same app.json works on SDK 50, 51, and 52 with no extra installation. For SDK 49 and earlier, the expo-privacy-manifest-polyfill-plugin covers the gap by writing the manifest in a prebuild hook. Run npx expo prebuild after every config change to regenerate the ios folder, then commit the generated PrivacyInfo.xcprivacy if your repo tracks the ios directory.
The reason an app needs to repeat declarations that a pod already ships is that Apple does not consistently parse pod-level manifests during App Store ingest. Expo's guide flags this directly: even when a dependency includes its own PrivacyInfo.xcprivacy, copying the same reason codes into the app-target config is the safer move.
How do you find the right reason codes for your dependencies?
The short answer is to grep node_modules for PrivacyInfo.xcprivacy and read the NSPrivacyAccessedAPITypeReasons array from each match. Every Expo SDK package that touches a required reason API ships one, and a large share of community packages now do the same.
A one-liner from the repo root will surface every dependency manifest in the project:
find node_modules -name "PrivacyInfo.xcprivacy" -not -path "*/Pods/*"
Open each file, copy the NSPrivacyAccessedAPIType and NSPrivacyAccessedAPITypeReasons pairs, and merge them into the privacyManifests block in app.json. When two dependencies declare the same category with different reason codes, list both codes in the same NSPrivacyAccessedAPITypeReasons array. Apple accepts multiple reasons per category.
The five required reason API categories and their most common codes follow Apple's describing use of required reason API page:
| Required reason API category | NSPrivacyAccessedAPIType value | Common code | Typical caller |
|---|---|---|---|
| UserDefaults | NSPrivacyAccessedAPICategoryUserDefaults | CA92.1 | AsyncStorage, MMKV, in-app settings |
| File timestamp | NSPrivacyAccessedAPICategoryFileTimestamp | C617.1 | image caches, file pickers |
| System boot time | NSPrivacyAccessedAPICategorySystemBootTime | 35F9.1 | uptime metrics, performance SDKs |
| Disk space | NSPrivacyAccessedAPICategoryDiskSpace | E174.1 | download managers, video apps |
| Active keyboard | NSPrivacyAccessedAPICategoryActiveKeyboard | 3EC4.1 | analytics, locale detection |
The reason codes are short, opaque strings. Apple's documentation lists the full text of each one. Picking a code you do not actually qualify for is its own rejection path: a reviewer who sees CA92.1 (read or write data in app group container) on an app with no app group entitlement will flag it.
For builders who want an automated read of the compiled IPA before submission, scanning the binary for required reason API calls that are missing from PrivacyInfo.xcprivacy, PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission analysis aligned with OWASP MASVS for no-code and vibe-coded apps. The scanner walks the binary's symbol table and lists every required reason API touched, against the manifest the build ships.
When do you need the bare workflow approach instead?
The short answer is when you have ejected from prebuild, when your ios folder is tracked manually, or when you ship a custom native module that needs a manifest entry the app.json schema does not support yet. In those cases, edit the file in Xcode and remove the privacyManifests block from app.json to avoid drift.
The bare workflow steps from Expo's privacy manifests guide are simple: right-click the iOS app target in Xcode, choose New File from Template, pick App Privacy under Resource, and Xcode generates a PrivacyInfo.xcprivacy file. The same four top-level keys apply, edited through Xcode's property list editor instead of JSON.
The trap is keeping both the app.json config and the manual Xcode file in the same repo. Prebuild will overwrite the manual file on the next run, which can erase a custom NSPrivacyCollectedDataTypes block you spent an afternoon writing. Pick one source of truth: managed teams keep app.json; bare teams delete the privacyManifests block from app.json and rely on the Xcode file.
For teams using continuous native generation but tracking the ios folder, add PrivacyInfo.xcprivacy to .gitignore and regenerate on each build. For teams that prebuild once and commit the result, treat the generated file as build output and regenerate after every config change.
What to watch out for
Three patterns surface in TestFlight rejection emails for Expo apps that should pass on paper.
The first is the SDK upgrade gap. An old Expo SDK still includes packages without privacy manifests, so the prebuild step generates an incomplete file. Running npx expo install --fix and rebuilding the prebuild output usually clears the warning. Anything below Expo SDK 50 needs the polyfill plugin or a manual file.
The second is the third-party SDK that ships a manifest only on its latest version. Sentry, Firebase, RevenueCat, and most analytics SDKs added PrivacyInfo.xcprivacy late in 2024, per Sentry's React Native privacy manifest documentation. Pinning an older version leaves the manifest empty and the app responsible for declaring those reason codes on the SDK's behalf. The fix is usually a version bump.
The third is the tracking flag mismatch. Setting NSPrivacyTracking to true in PrivacyInfo.xcprivacy while leaving Tracking off in App Store Connect's data collection survey triggers a validator warning, and the reverse triggers one too. The survey in App Store Connect and the manifest in the build must agree, otherwise the App Privacy section on the store listing reads as inconsistent.
A myth worth rejecting: there is no automated tool that produces a complete PrivacyInfo.xcprivacy on its own. The reviewer reads the binary, the manifest, and the App Store Connect survey together, and any of the three being out of step is enough for a rejection. Treat the file as a declaration that has to match reality, not as a checkbox.
Key takeaways
- For a managed Expo project, expo.ios.privacyManifests in app.json is the right place; prebuild generates PrivacyInfo.xcprivacy from it.
- For a bare Expo project, edit the file by hand in Xcode and remove the app.json block to avoid prebuild overwriting your work.
- Grep node_modules for PrivacyInfo.xcprivacy after every dependency change, merge the reason codes into the app-target manifest, and accept duplicate categories.
- The five required reason API categories cover almost every common case; pick the code that matches your actual call site, not the one that sounds closest.
- For teams that want an external read of the compiled IPA before App Store upload, checking the required reason APIs in the binary against the shipped manifest, PTKD.com (https://ptkd.com) is one of the platforms focused on this layer of pre-submission analysis.



