Journal

Expo Missing Purpose String Rejection: The Real 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.

Expo Missing Purpose String Rejection: The Real Fix: a vivid neon 3D App Store icon on an orange, pink and blue gradient

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 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:

{
  "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 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?

StringReview outcomePrompt outcomeVerdict
”Scan documents to attach them to your reports.”PassesUsers grant; the benefit is theirsThe bar: specific action, user’s benefit, one sentence
”This app needs camera access.”5.1.1 magnetUsers hesitate; whose need?Present but worthless; the classic AI-generated filler
”Camera.”Rejection baitDistrustNever
A string for an unused capabilityScrutiny of the whole appn/aRemove 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 and the background location disclosures.

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. The rejection most often lands at the TestFlight gate, the same pipeline stage covered in the Replit-to-iPhone guide, 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 hold you to.

The screens behind the permissions, the scanner, the recorder, the map, start as ever from a free VP0 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.

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) 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.

Questions VP0 users ask

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.

Part of the React Native & Expo: Mobile Frontend Architecture hub. Browse all VP0 topics →

Keep reading

Fix Replit Agent React Native Expo Crashes: Triage: the App Store logo on a glass tile over a blue gradient with bubbles
Workflows 5 min read

Fix Replit Agent React Native Expo Crashes: Triage

Triage Replit Agent React Native Expo crashes by symptom: bundler failures, red-screen errors, native-module deaths, and how to stop the agent's fix loop.

Lawrence Arya · June 5, 2026
FlatList Memory Lag With Maps in React Native: Fixes: the App Store logo as a glossy glass icon on a purple and blue gradient with floating bubbles
Workflows 5 min read

FlatList Memory Lag With Maps in React Native: Fixes

Fix FlatList memory and lag in React Native map screens: the map-per-row trap, memoized rows, windowing settings, and when FlashList is the real answer.

Lawrence Arya · June 5, 2026
Fixing Claude React Native Reanimated Errors, Fast: the App Store logo as a frosted glass icon on a pink and blue gradient with bubbles
Workflows 5 min read

Fixing Claude React Native Reanimated Errors, Fast

The Reanimated errors Claude-generated code hits most: the babel plugin rule, version mismatches, worklet violations, and the prompts that fix each one.

Lawrence Arya · June 5, 2026
Pod Install ffi Error on Apple Silicon: The Real Fix: the App Store logo as a glossy glass icon on a purple and blue gradient with floating bubbles
Workflows 5 min read

Pod Install ffi Error on Apple Silicon: The Real Fix

Fix the pod install ffi incompatible-architecture error on Apple Silicon: why the Ruby ffi gem breaks, the Homebrew fix, and the Rosetta trap to avoid.

Lawrence Arya · June 5, 2026
Expo Push Notifications Not Working From Lovable? Fix It: a glossy App Store icon on a blue, pink and orange gradient with bubbles
Workflows 5 min read

Expo Push Notifications Not Working From Lovable? Fix It

Push notifications dead in your Lovable export? It is almost always setup, capability, APNs, permission, or token, not code. Here are the causes and fixes.

Lawrence Arya · June 1, 2026
Fatal Error: Array Bounds in AI Swift Code: 4 Families: a glass iPhone app-grid icon on a mint and teal gradient
Workflows 4 min read

Fatal Error: Array Bounds in AI Swift Code: 4 Families

Fix the Index-out-of-range crashes AI writes in Swift: parallel-array assumptions, force-indexing, stale ForEach indices, and the brief that prevents all four.

Lawrence Arya · June 5, 2026