# Expo Missing Purpose String Rejection: The Real Fix

> By Lawrence Arya, Founder & CEO of VP0. Published 2026-06-05. 5 min read.
> Source: https://vp0.com/blogs/react-native-expo-missing-purpose-string-rejection-fix

The rejection names a key you have never heard of for a feature you may not even use. Both halves have clean fixes, and one of them is deleting code.

**TL;DR.** The missing purpose string rejection (ITMS-90683, or a crash the moment a permission prompt should appear) means your binary references a protected API, camera, microphone, photos, location, without the matching NSUsageDescription key in Info.plist. In Expo the fix lives in app.json: most config plugins accept the string directly, and anything else goes under ios.infoPlist. The diagnostic fork matters: if your app uses the feature, write a specific, user-benefit purpose string, generic boilerplate draws its own 5.1.1 rejection; if it does not, find the dependency dragging the API in and remove or reconfigure it, because a purpose string for an unused capability is the wrong fix. AI agents cause this weekly by adding image pickers without plist edits; the prompt clause that prevents it costs one line.

## What is this rejection actually telling you?

Two presentations, one disease. At upload, App Store Connect bounces the build with ITMS-90683 naming a missing purpose string; at runtime, the app dies the instant a permission prompt should have appeared. Either way the meaning is identical: **the binary references a protected API, and Info.plist lacks the matching usage-description key**, the [NS...UsageDescription entries Apple documents](https://developer.apple.com/documentation/bundleresources/information_property_list/nscamerausagedescription) for camera, microphone, photo library, location, contacts, and the rest.

The diagnostic fork is the whole triage: **does your app actually use the named capability?** If yes, you owe a real purpose string. If no, something in your dependency tree references the API anyway, and the right fix is removal, not boilerplate, because shipping a camera explanation for a camera you never open is the wrong kind of compliance.

## Where does the fix live in an Expo project?

In `app.json`, two ways, both landing in Info.plist at prebuild:

```jsonc
{
  "expo": {
    "plugins": [
      ["expo-camera", { "cameraPermission": "Scan documents to attach them to your reports." }],
      ["expo-location", { "locationWhenInUsePermission": "Show walks on the map while you record them." }]
    ],
    "ios": {
      "infoPlist": {
        "NSMicrophoneUsageDescription": "Record voice notes you choose to attach."
      }
    }
  }
}
```

The config plugins for capability-bearing packages ([Expo's permissions guide](https://docs.expo.dev/guides/permissions/) catalogs them) accept the strings as options, which is the preferred route because the plugin keeps key and capability in sync. Anything without a plugin goes under `ios.infoPlist` directly. Rebuild, `npx expo prebuild` then a new binary, because **purpose strings live in the build, not in JavaScript**; no hot reload ever fixes this class.

When the named capability is a stranger, hunt the reference: search node_modules for the key name or API, check recently added packages first, and remember the usual suspects, kitchen-sink media libraries, analytics SDKs with optional capture features, and the agent-added image picker from three Tuesdays ago. Configure the library to exclude the capability, replace it with a narrower one, or supply the string only if the transitive use is real.

## What makes a purpose string good instead of merely present?

| String | Review outcome | Prompt outcome | Verdict |
| --- | --- | --- | --- |
| "Scan documents to attach them to your reports." | Passes | Users grant; the benefit is theirs | The bar: specific action, user's benefit, one sentence |
| "This app needs camera access." | 5.1.1 magnet | Users hesitate; whose need? | Present but worthless; the classic AI-generated filler |
| "Camera." | Rejection bait | Distrust | Never |
| A string for an unused capability | Scrutiny of the whole app | n/a | Remove the capability instead |

The string appears verbatim in the system permission prompt, which makes it conversion copy as much as compliance: a user deciding whether to grant camera access is reading your one sentence, and **the version that names their benefit wins grants** as well as review. Write it in the user's language (it localizes with the app), keep it to one sentence, and let it match what the feature visibly does, the same say-what-happens honesty that runs through [the transcription app's mic strings](/blogs/swiftui-transcribe-audio-template-whisper/) and [the background location disclosures](/blogs/background-geolocation-tracking-ai-prompt/).

## How does this fit the AI-builder workflow?

As the canonical skipped side-requirement. Agents add capability-bearing packages fluently, an image picker for the avatar feature, a location hook for the store finder, and the plist edit is precisely the kind of adjacent obligation they omit unprompted; the build then fails days later at upload, wearing an error code nobody recognizes. The standing prompt clause costs one line: **"any dependency touching camera, microphone, photos, location, or contacts ships with the matching app.json permission string and a one-line justification."**

Triage discipline transfers from the rest of the toolchain series: read the actual error (ITMS codes name the exact key), fix the build configuration, and keep the agent away from refactoring innocent JavaScript, the same machine-not-code instinct as [the ffi architecture fix](/blogs/pod-install-ffi-error-apple-silicon-fix/). The rejection most often lands at the TestFlight gate, the same pipeline stage covered in [the Replit-to-iPhone guide](/blogs/replit-agent-expo-run-on-physical-iphone/), and clearing it there, before the 10,000-tester external track, is the cheap place to clear it. The capability audit is also a privacy audit for free: every purpose string your app ships is a promise [the deeper 5.1.1 data-collection rules](/blogs/ios-guideline-5-1-1-data-collection-ui-template/) hold you to.

The screens behind the permissions, the scanner, the recorder, the map, start as ever from a free [VP0](https://vp0.com) design generated by Claude Code or Cursor; the permission strings are the one part of the feature the design cannot carry, which is exactly why they belong in the prompt.

The sibling build-time registration, custom fonts that vanish between preview and device, follows the same plist-and-naming logic in [the Rork Xcode fonts fix](/blogs/rork-xcode-custom-fonts-fix/).

## Key takeaways: missing purpose string in Expo

- **One disease, two faces**: ITMS-90683 at upload or a crash at the prompt; the binary references a protected API without its NSUsageDescription key.
- **The fork is the triage**: real feature → real string; stranger capability → find and remove the dependency reference, never boilerplate it.
- **Fix in app.json, rebuild**: plugin options for capability packages, ios.infoPlist for the rest; purpose strings live in the build, not in JS.
- **Specific, user-benefit, one sentence**: the string is conversion copy in the system prompt and a shield against [the guidelines' privacy rules (5.1.1)](https://developer.apple.com/app-store/review/guidelines/) in review.
- **Put the obligation in the agent's standing prompt**, audit capabilities at the TestFlight gate, and start the screens from a free VP0 design.

## Frequently asked questions

**How do I fix the missing purpose string rejection in Expo?** Name the API from the error, then either supply the string (plugin options or ios.infoPlist in app.json) and rebuild, or remove the dependency that references a capability you never use.

**What is Apple actually requiring?** A user-facing usage-description key for every protected resource the binary references, per the NS...UsageDescription documentation; missing keys reject at upload or crash at the prompt.

**Why does the rejection name a feature my app doesn't use?** A dependency references it: kitchen-sink media libraries, SDK optional features, agent-added packages. Search, then exclude, replace, or honestly justify.

**What makes a purpose string pass review instead of drawing 5.1.1?** One specific sentence stating the user's benefit ("Scan documents to attach to your reports"), never generic need-statements; it doubles as grant-rate copy.

**How do I stop AI agents from reintroducing this?** A standing clause: capability-touching dependencies ship with their app.json permission string and a one-line justification, enforced at review like any other diff rule.

## Frequently asked questions

### How do I fix the missing purpose string rejection in Expo?

Identify which protected API the message names, then either supply the string or remove the reference. In Expo, permission strings ride config plugins (expo-camera, expo-location accept them as plugin options) or sit under ios.infoPlist in app.json; rebuild and the key lands in Info.plist. If the app never uses the capability, the right fix is removing the dependency that references it, not adding boilerplate.

### What is Apple actually requiring?

Every access to a protected resource must carry a purpose string, the NS...UsageDescription keys like NSCameraUsageDescription, shown to the user in the permission prompt. Apple's documentation is explicit that the string explains why the app needs the access; a binary that references the API without the key is rejected at upload or crashes at the prompt.

### Why does the rejection name a feature my app doesn't use?

A dependency references the API even if you never call it: an SDK with an optional photo feature, a kitchen-sink media library, an agent-added package. Search your node_modules and native config for the API name, then decide: configure the library to exclude it, replace the library, or, if it is genuinely needed transitively, supply the string honestly.

### What makes a purpose string pass review instead of drawing 5.1.1?

Specificity and user benefit: 'Scan documents to attach them to your reports' passes where 'This app needs camera access' invites scrutiny. State what the user gets, in their language, one sentence. The string appears in the system prompt, so it is also conversion copy: a good one measurably improves permission grant rates.

### How do I stop AI agents from reintroducing this?

One standing prompt clause: 'any dependency that touches camera, microphone, photos, location, or contacts must come with the matching app.json permission string and a one-line justification.' Agents add capability-bearing packages readily, an image picker for an avatar feature, and the plist edit is exactly the kind of side requirement they skip without instruction.

---
*Published on the [VP0 Journal](https://vp0.com/blogs). Free to read, index and cite with attribution.*
