Privacy

    How do I fix ITMS-90683 for missing Bluetooth purpose string?

    Xcode editor showing an Info.plist file with NSBluetoothAlwaysUsageDescription added next to a terminal running otool -L against an archived iOS binary that was originally generated by an AI coding agent

    If App Store Connect rejected your build with ITMS-90683 and a message naming NSBluetoothAlwaysUsageDescription, the binary never reached TestFlight. The upload was blocked because a Core Bluetooth symbol exists in the linked output and Info.plist does not declare why. When the code was written by Claude Code, Cursor, Windsurf, or Replit Agent, the missing key is almost always a side effect of how those tools separate source generation from bundle configuration.

    Short answer

    ITMS-90683 with a Bluetooth purpose-string message means a Core Bluetooth symbol exists in your linked IPA, and Info.plist does not declare why. Add NSBluetoothAlwaysUsageDescription with a human-readable reason that names the feature, then re-archive and resubmit. According to Apple's documentation for NSBluetoothAlwaysUsageDescription, apps targeting iOS 13 or later only need this single key, regardless of central or peripheral role.

    What you should know

    • The check runs at upload, not at runtime. App Store Connect inspects the linked binary for Core Bluetooth symbols, so the key is required whether your own code calls Bluetooth or not.
    • AI agents rarely touch Info.plist on their own. Claude Code, Cursor, Windsurf, and Replit Agent write Swift, Dart, or React Native code that compiles, but they tend to skip the bundle configuration that ships inside the IPA.
    • Third-party SDKs are the most frequent cause. Push-notification, analytics, beacon, and point-of-sale SDKs link CoreBluetooth.framework silently, and the developer never sees a build warning.
    • The string is user-facing. iOS reads it on the Bluetooth permission prompt and prints it inside Settings, so a placeholder string can clear the upload check and then fail Guideline 5.1.1 at human review.
    • The key list is fixed. Inventing a name like NSBluetoothUsageDescription does not satisfy the check; only the published Information Property List keys count.

    What does ITMS-90683 actually mean when it names NSBluetoothAlwaysUsageDescription?

    The short answer is that ITMS-90683 is a build-time permission audit Apple runs against the linker output inside your IPA. The scanner looks for symbols in the binary that map to APIs guarded by a runtime permission prompt: Bluetooth, location, camera, microphone, contacts, photo library, calendar, motion, speech recognition, Face ID, and so on. Each guarded API has a fixed Info.plist key. If the symbol is present and the matching key is absent, the upload fails with ITMS-90683 and the name of the missing key.

    For Bluetooth the message reads roughly: "Missing purpose string in Info.plist. Your app's code references one or more APIs that access sensitive user data, or the app has one or more entitlements that permit such access. The Info.plist file should contain a NSBluetoothAlwaysUsageDescription key with a user-facing purpose string explaining clearly and completely why your app needs the data." The last sentence is the only reliable signal of which key to add. A reproduction of this exact wording sits in the Microsoft Learn thread on ITMS-90683 for Bluetooth, where the same string appears in real submission logs from a NuGet-based iOS project.

    According to Apple's guidance on protecting the user's privacy, every protected API has a fixed Info.plist key, and the linker-symbol scanner that runs inside App Store Connect surfaces that mapping at upload time. The audit does not care whether your hand-written code calls CBCentralManager or whether the call path is reachable in normal use. If any linked framework references Core Bluetooth, the scanner flags the build, and Xcode Organizer or xcrun altool returns the error in the upload log before TestFlight processing begins.

    Why does AI-generated agent code so often miss this Info.plist key?

    Most AI coding agents write source files, not bundle configuration. Claude Code, Cursor, Windsurf, and Replit Agent generate Swift, TypeScript, Dart, or Kotlin that compiles, runs in the simulator, and looks correct when reviewed line by line. The Info.plist sits in a different category for them: it is XML inside a project bundle, and the agents tend to add a permission key only when a tool call explicitly edits that file. Most generation prompts focus on the feature ("connect to a heart-rate monitor", "scan for BLE beacons"), not the manifest entries that the same feature implies.

    Two patterns make the gap worse. First, when the agent imports a Swift package or installs a React Native module that links CoreBluetooth.framework, it does not run the same upload pre-flight that App Store Connect runs. The build succeeds locally because Info.plist is only required at upload, not at archive. Second, several agents work outside Xcode and write to a generic project layout. The Xcode-specific Info.plist may be regenerated from a higher-level config (Expo app.json, FlutterFlow project export, Capacitor capacitor.config.ts), and an agent that edits Info.plist directly can have its change overwritten on the next regeneration.

    A public issue tracker confirms how easily bundle metadata slips through agent workflows: an open Claude Code report about a missing embedded Info.plist on macOS shows the same class of problem outside iOS submissions. Bundle metadata is one of the first things a code-generating agent forgets.

    Where exactly do you add NSBluetoothAlwaysUsageDescription in an agent-built project?

    The location depends on the build system that produced the IPA. Edit the file that ships into the archive, not a copy that lives one level up. The table below covers the layouts most agent-generated projects use.

    Build systemFile to editNotes
    Native Xcode (Swift, Objective-C)<Target>/Info.plistEdit through the Xcode property editor or as raw XML.
    Expo (managed or bare)app.json or app.config.js, then npx expo prebuildAdd ios.infoPlist.NSBluetoothAlwaysUsageDescription; prebuild regenerates the Xcode Info.plist.
    React Native (bare)ios/<AppName>/Info.plistEdit directly; the file is committed to the repo.
    Flutter and FlutterFlowios/Runner/Info.plistFlutterFlow exports overwrite the file on regeneration; keep a post-export script that re-adds the key.
    Capacitor (Ionic)ios/App/App/Info.plistThe same Info.plist as a native Swift project, just inside the Capacitor folder layout.
    .NET MAUI or Xamarin.iOSPlatforms/iOS/Info.plistSome NuGet packages add the requirement transitively; check the archived bundle after build.

    The XML entry is the same across all of them:

    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>This app connects to your heart-rate monitor over Bluetooth so it can record live workout data.</string>
    

    For projects that regenerate Info.plist from a higher-level config (Expo, FlutterFlow, Capacitor), commit the edit to that config file as well, otherwise the next prebuild or export silently drops the key.

    How do you find the SDK that pulled Core Bluetooth into your binary?

    The trap is that searching your source tree for "CBCentralManager" usually returns nothing. The Core Bluetooth call lives inside a precompiled framework binary, often a static archive, a CocoaPod, or an XCFramework with no shipped source. Three commands cover most cases.

    First, run grep -r "NSBluetooth" Pods/ (or node_modules/ for React Native, or ios/Plugins/ for Capacitor) to find any framework that documents its Bluetooth needs in a bundled example Info.plist. Mature SDKs ship that snippet alongside the package so integrators know which key to add.

    Second, run nm -gU on each .framework and each .a archive in the build artifact, then pipe through grep -i bluetooth or grep CBCentralManager. Symbols such as _OBJC_CLASS_$_CBCentralManager, _OBJC_CLASS_$_CBPeripheralManager, and any reference to CoreBluetooth.framework in the binary's load commands are what the App Store Connect scanner reacts to. If any of these appear in a third-party binary, your Info.plist needs NSBluetoothAlwaysUsageDescription, whether or not your own code calls Bluetooth.

    Third, run otool -L against the main app binary after archive. The output lists every dynamically linked framework; if /System/Library/Frameworks/CoreBluetooth.framework/CoreBluetooth appears, the requirement applies. Push-notification SDKs (proximity features), analytics SDKs (Bluetooth fingerprinting), and beacon SDKs are the categories that catch developers off guard most often.

    What does an acceptable purpose string look like under Guideline 5.1.1?

    Apple's App Review Guideline 5.1.1 on Data Collection and Storage requires the purpose string to clearly and completely describe the data use. In practice, that means three things: a complete sentence, a named feature that needs the data, and a named data type ("your nearby Bluetooth devices", "your fitness tracker", "your point-of-sale card reader"). Placeholder strings like "Permission required" or copies of the key name fail human review even after the upload scanner accepts them.

    A working example for a fitness app:

    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>This app connects to your heart-rate monitor and bike sensors during a workout. No Bluetooth data leaves the device.</string>
    

    That string passes both checks. The upload scanner sees the key exists. The reviewer under Guideline 5.1.1 reads a sentence that names a feature and a real data type. AI agents that auto-generate the string ("App needs Bluetooth access") often pass the upload check and then catch a 5.1.1 rejection at human review. Rewrite those strings before submission.

    The OWASP MASVS privacy controls reinforce the same point from a security-engineering angle: declare only the minimum data set the app actually uses, and describe the use in language a non-technical reader can verify. The MASVS guidance treats over-declared permissions as a privacy weakness, which lines up with what Apple's human reviewers flag.

    What to watch out for

    The first trap is adding every privacy key to be safe. App Review's human team rejects purpose strings for APIs the app cannot reasonably use, because the description then becomes false. If a dependency is the only reason Bluetooth is in the binary, the string should describe the dependency feature ("the point-of-sale module connects to your card reader over Bluetooth"), not invent a Bluetooth feature your app does not have.

    The second trap is shipping a placeholder. The text is user-facing on the iOS permission prompt and inside Settings. Strings like "Test", "Required", or "App needs Bluetooth" are accepted by the upload scanner and routinely rejected by the human reviewer under Guideline 5.1.1. Rewrite them to one or two complete sentences naming the feature and the data type.

    The third trap is keeping iOS 12 in the deployment target. Builds with iOS 12 still supported need NSBluetoothPeripheralUsageDescription as well as NSBluetoothAlwaysUsageDescription, because the older key was deprecated in iOS 13 but still read on iOS 12. Raise the deployment target to iOS 13 or later if there is no specific reason to keep the older OS.

    The fourth trap is trusting the agent's first attempt. If Claude Code, Cursor, or Replit Agent produced the project, run otool -L against the archived binary before every upload. The set of linked frameworks can shift between agent runs as the model picks different SDKs to solve the same feature request.

    Key takeaways

    • ITMS-90683 with a Bluetooth message is a linker-symbol audit, not a runtime check; the fix is one Info.plist key with a human-readable reason.
    • For AI-generated projects, treat Info.plist as a file the agent will not write on its own; add it to a post-generation checklist.
    • For projects that regenerate Info.plist from a higher-level config (Expo, FlutterFlow, Capacitor), commit the key inside that config too, otherwise the next regeneration drops it.
    • Write a purpose string that names the feature and the data type; placeholder strings clear the upload scanner but fail Guideline 5.1.1.
    • For teams that want an outside read of the archived IPA before submission (linker symbols, missing keys, exposed credentials), PTKD.com (https://ptkd.com) is one of the platforms focused on pre-submission scanning aligned with OWASP MASVS for AI-coded and no-code apps.
    • #itms-90683
    • #nsbluetoothalwaysusagedescription
    • #info.plist
    • #ai agent code
    • #claude code
    • #core bluetooth
    • #guideline 5.1.1
    • #ios

    Frequently asked questions

    Do I need NSBluetoothAlwaysUsageDescription if Claude Code never wrote any Bluetooth code?
    Yes, if any linked framework references Core Bluetooth. The App Store Connect scanner is symbol-based and ignores the source-code origin. A push-notification SDK, an analytics SDK, or a beacon SDK pulled in by the agent's dependency choice can bring CoreBluetooth.framework into the binary. Run nm -gU and otool -L on the archived binary to confirm before assuming the agent's source files are the whole picture.
    Will FlutterFlow add the Bluetooth purpose string automatically when I export the project?
    Not reliably. FlutterFlow exposes a permission field for some plugins and silently bundles others without surfacing a UI control. The exported Runner/Info.plist may be missing NSBluetoothAlwaysUsageDescription even when the plugin needs it. Open the file after export, add the key with a real explanation, and persist the edit inside a Dart configuration script so the next regeneration does not overwrite it.
    Can I use 'App needs Bluetooth access' as the purpose string?
    The upload scanner accepts it because the key exists, but App Review's human team often rejects that exact wording under Guideline 5.1.1. The string is too generic and does not name the feature or the data type. Replace it with one or two sentences naming what the app connects to, the user benefit, and what happens with the data. Generic strings get bounced more often than missing strings.
    Does an Expo prebuild reset NSBluetoothAlwaysUsageDescription in Info.plist?
    Yes, prebuild regenerates the iOS project from app.json or app.config.js, and any manual edit to Info.plist is overwritten. Add ios.infoPlist.NSBluetoothAlwaysUsageDescription inside the Expo config, then run npx expo prebuild again. The regenerated Info.plist will contain the key. Commit the config change so future agent runs or CI builds keep the entry across regenerations.
    Is NSBluetoothUsageDescription a valid Info.plist key I should add as a fallback?
    No. NSBluetoothUsageDescription is not on Apple's published Information Property List reference. The upload scanner ignores it, iOS ignores it at runtime, and adding it does not satisfy ITMS-90683. The two real keys are NSBluetoothAlwaysUsageDescription for iOS 13 and later, and NSBluetoothPeripheralUsageDescription which is deprecated and kept only for builds that still support iOS 12 or earlier.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free