●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Auditing Privacy Manifests for Rork-Generated Expo Apps — A One-Day Pre-Submission Workflow for Indie Developers
A pre-submission workflow for indie developers shipping Rork-generated Expo apps. Walks through how to enumerate every dependency, detect missing PrivacyInfo.xcprivacy files, and ship without ITMS-91053 rejections — based on twelve years of personal app development.
When you are shipping iOS apps on your own, getting back "ITMS-91053: Missing API declarations" from App Store Connect on release day tends to eat the entire day. I have been running wallpaper and mindfulness apps as an indie developer since 2014, with a combined fifty million downloads across the catalog, and after Privacy Manifests became mandatory in 2024, the rate of last-minute rejections rose noticeably.
The hardest part was that the rejections almost never came from code I had written. A native library deep inside the Rork-generated Expo project was calling UserDefaults, and one missing declaration was enough to halt the whole submission. Apple's automated checks are stricter than human reviewers, and the reason text can take hours to surface. If you only react after the fact, your monthly release plan starts to slip.
This guide lays out how to move from "post-rejection mode" to "pre-submission audit mode" for Rork-generated Expo and React Native apps. The goal is to walk you through a half-day to one-day workflow that exhaustively reviews the SDK chain, so that you can leave with a working npm ls + find-based audit script in your repo by the end of reading.
Why "react after rejection" is too slow
Reacting after rejection is heavier than it looks. The shortest realistic loop after a single rejection looks like this. Two to six hours for the rejection email to arrive after upload. One to three hours to pinpoint which SDK is causing it. Thirty minutes to two hours to edit PrivacyInfo.xcprivacy and rebuild via EAS Build. Another hour for re-upload and processing. Even on a good day you are losing half a day, and on a bad day a day and a half.
In spring 2024 I went through this loop three times in a row on a new wallpaper app. Builds that passed before AdMob was added started failing with UserDefaults-related Required Reason violations after adding AdMob. Adding RevenueCat then surfaced an NSPrivacyAccessedAPICategoryDiskSpace requirement. Every SDK addition cost one rejection, and the release date slipped a full week.
Switching to a pre-submission audit eliminated almost all ITMS-91053 rejections from my release flow. The audit shifted the work from "gambling on review luck" to a process I can reproduce.
Understanding the structure of the SDK chain
Privacy Manifests are hierarchical. It is rare for a single root PrivacyInfo.xcprivacy to cover everything; each native module needs its own. Apple's automated review runs against the built .ipa as a whole, so a single missing manifest anywhere in the dependency tree fails the entire submission.
A typical Rork-generated Expo app has three layers of dependencies.
The app itself (iOS project generated from app/ and app.json)
The Expo SDK family (expo-* modules, roughly 20–40 packages)
Third-party React Native libraries (ads, billing, analytics, storage, notifications — five to fifteen packages)
You only need to author the manifest for the first layer yourself. The other two depend on whether the SDK author has shipped one. As of May 2026, Expo SDK 53 and 54 cover essentially every module, but third-party libraries are still uneven.
A particularly tricky case is Expo Config Plugins that inject native code dynamically. Because Plugin-driven files are generated at EAS Build time, grepping the local ios/ folder will miss them. I once spent a full day discovering that the output of expo-notifications' plugin did not include PrivacyInfo.xcprivacy. Once you know that vector exists, the fix is mechanical, but if you do not know to look there, it can vaporize a release window.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A reproducible 30-minute script that lists every native dependency in a Rork-generated app and flags which ones lack PrivacyInfo.xcprivacy
✦A May 2026 cheat sheet for AdMob, Firebase, RevenueCat, MMKV, Sentry, and other SDKs that personal developers actually ship
✦A pre-submission workflow that turns post-rejection scramble into a calm, repeatable process — backed by real ITMS-91053 case studies
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The half-day track (about three hours) is for releases that add one or two SDKs. The full-day track (seven to eight hours) is for major Expo SDK upgrades or any release where Rork's AI suggestions reshaped the dependency tree significantly. I use the half-day track for wallpaper app updates and the full-day track when I move a mindfulness app onto the new annual iOS major version.
The half-day track looks like this.
0:00–0:30 — Capture the dependency tree with npm ls --production --depth=5 and run the PrivacyInfo.xcprivacy discovery script
0:30–1:30 — List the missing-manifest packages and check whether upgrading to the latest version resolves them
1:30–2:30 — Merge or write the additional manifests by hand and run a local EAS Build pass
2:30–3:00 — Validate the whole .ipa with the same automated check xcrun altool --validate-app runs
The full-day track adds tracking of files emitted by Expo Config Plugins, a diff check against Apple Developer's "Required Reason API documentation," and a reconciliation between the Privacy Nutrition Label and the manifest.
Step 1: Extract the dependency tree accurately
The first task is to enumerate every SDK that ends up in the build. You could ask Rork's chat to do it, but the most reproducible path is local.
# Run from the project root of the Rork-generated appnpm ls --production --depth=5 --json > /tmp/deps-tree.json# Keep only packages that are likely to ship native codejq -r ' [.. | objects | select(.dependencies != null) | .dependencies | keys[]] | unique[]' /tmp/deps-tree.json \ | grep -E "^(react-native-|expo-|@react-native-|@expo/|@stripe/|@sentry/|firebase|@react-native-firebase|react-native-google-mobile-ads|react-native-mmkv|react-native-purchases)" \ > /tmp/native-deps.txtwc -l /tmp/native-deps.txt
I set --depth=5 because Expo SDK's inner dependencies (expo-modules-core and so on) typically live two or three levels deep. --depth=Infinity produces files large enough to slow the subsequent processing.
For my main wallpaper app, which has about twenty-two top-level dependencies, this script returns forty to fifty-five lines. The number shifts each time Rork rewrites code with the AI, so re-running it just before release gives you a clean diff. That diff is the starting point for the audit.
Step 2: Check PrivacyInfo.xcprivacy coverage in bulk
For each dependency, check whether PrivacyInfo.xcprivacy ships with it. Doing this one library at a time turns into a find slog, so I keep this shell script as _tools/audit-privacy-manifest.sh.
#!/usr/bin/env bash# audit-privacy-manifest.sh — Check PrivacyInfo.xcprivacy coverage across native depsset -euo pipefailNATIVE_DEPS="${1:-/tmp/native-deps.txt}"REPORT="/tmp/privacy-audit-$(date +%Y%m%d-%H%M).tsv"printf "package\tstatus\tpath\n" > "$REPORT"while IFS= read -r pkg; do base="node_modules/$pkg" if [ ! -d "$base" ]; then printf "%s\tmissing-dir\t-\n" "$pkg" >> "$REPORT" continue fi found=$(find "$base" -name "PrivacyInfo.xcprivacy" 2>/dev/null | head -1 || true) if [ -n "$found" ]; then printf "%s\thas-manifest\t%s\n" "$pkg" "$found" >> "$REPORT" else printf "%s\tNO-MANIFEST\t-\n" "$pkg" >> "$REPORT" fidone < "$NATIVE_DEPS"echo "report: $REPORT"column -t -s $'\t' "$REPORT" | head -30
When the script finishes, the NO-MANIFEST rows are the SDKs that need immediate work, and has-manifest rows can be left alone. On my latest wallpaper app, out of forty-eight dependencies, six were NO-MANIFEST. Three of those were resolved by npm update because the latest releases shipped manifests, and the remaining three needed manual handling.
Step 3: A decision flow for SDKs missing a manifest
NO-MANIFEST rows are not a panic moment. Most clear up within thirty minutes if you walk through the following stages.
The first stage is "does the latest version ship one?" Run npm view <package> versions to scan history and npm view <package@latest> files to inspect what gets bundled. If a manifest is in there, npm install <package>@latest is the entire fix. In my experience, roughly eighty percent of SDKs that had a major release before mid-2024 shipped Privacy Manifest support during 2025.
The second stage is "is there a tracking issue?" The query https://github.com/<org>/<repo>/issues?q=PrivacyInfo almost always surfaces a relevant thread. If a fix is merged, you can wait for the next release or apply patch-package immediately.
The third stage is "ship one yourself." This is the last resort when the library author is unresponsive or you are using a fork. You write a postinstall script that drops a node_modules/<package>/ios/PrivacyInfo.xcprivacy file into place. Here is the snippet I use for one older React Native animation library.
// scripts/inject-privacy-manifest.js// Last-resort injection when the upstream package has no PrivacyInfo.xcprivacyconst fs = require("fs");const path = require("path");const targets = [ { pkg: "react-native-some-old-lib", iosDir: "ios", manifest: "manifests/some-old-lib.xcprivacy", },];for (const t of targets) { const dest = path.join("node_modules", t.pkg, t.iosDir, "PrivacyInfo.xcprivacy"); if (fs.existsSync(dest)) continue; fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(t.manifest, dest); console.log(`injected: ${dest}`);}
Adding node scripts/inject-privacy-manifest.js to package.jsonscripts.postinstall ensures the file is restored on every npm install. patch-package works for the same goal, but I find the file-based approach easier to track in code review.
Step 4: A cheat sheet of common SDKs (May 2026 snapshot)
This is the current status of the SDKs that I actively use across my wallpaper, mindfulness, and law-of-attraction apps. Versions are as of writing; always confirm with npm view <pkg>@latest immediately before release.
react-native-google-mobile-ads (AdMob) — Manifest shipped from 13.x. Requires UserDefaults: CA92.1 and SystemBootTime: 35F9.1. AdMob is one of the most painful to handle by hand, and 12.x or older needs manual work.
react-native-purchases (RevenueCat) — Manifest shipped from 9.x. Requires DiskSpace: E174.1 and UserDefaults: CA92.1. Be sure to keep the Privacy Nutrition Label in sync.
@sentry/react-native — Manifest shipped from 6.x with FileTimestamp: C617.1 and UserDefaults: CA92.1.
@react-native-firebase/app and friends — Manifest support starts at 22.x. Auth, Analytics, Messaging, and Crashlytics each have their own subpackage manifest.
react-native-mmkv — Manifest shipped from 3.x, declaring UserDefaults: CA92.1.
@react-native-async-storage/async-storage — Manifest shipped from 2.x, declaring UserDefaults: CA92.1.
expo-notifications, expo-file-system, expo-secure-store — Covered by Expo SDK 53 and later. No extra declarations required.
react-native-mixpanel — Quality varies between forks; if the latest does not ship a manifest, integrating the native Mixpanel iOS SDK directly is faster than waiting.
Anything not in this list, I check with npm view <pkg> files | grep -i privacy. If a manifest is bundled, adopt it. Otherwise, drop straight to the third stage of the decision flow above.
Step 5: Tighten the app-level manifest
After the SDK side is squared away, do the final pass on your own PrivacyInfo.xcprivacy. Rork-generated Expo apps store this in a slightly unusual place.
Stock Expo: app/PrivacyInfo.xcprivacy, with app.json declaring ios.privacyManifests
Bare workflow: ios/<AppName>/PrivacyInfo.xcprivacy
Rork-customized layouts: read expo.ios.privacyManifests in app.json
Here is the app.json template I have been using on wallpaper apps. It passes review with AdMob, RevenueCat, and Sentry installed, without further declarations on top.
Note that NSPrivacyTracking is false. ATT-free apps (most of my wallpaper apps fall into this) are fine that way, but when you enable AdMob's personalized ads, flip it to true and populate NSPrivacyTrackingDomains. My mindfulness apps do not show ATT prompts, so false; web-linked apps stay on true.
Step 6: Validate the build locally before the long EAS Build loop
Once the manifest set looks right, run a local build and inspect the .ipa before kicking off an EAS Build. Cloud builds have a long feedback loop, and you want to catch mistakes here.
# Local build (requires a Mac)npx expo prebuild --platform ios --cleancd ios && pod install && cd ..# Archive in Xcode and extract the ipaxcodebuild archive \ -workspace ios/MyApp.xcworkspace \ -scheme MyApp \ -configuration Release \ -archivePath /tmp/MyApp.xcarchive \ -destination "generic/platform=iOS"xcodebuild -exportArchive \ -archivePath /tmp/MyApp.xcarchive \ -exportPath /tmp/MyApp \ -exportOptionsPlist ios/export-options.plist# List every PrivacyInfo.xcprivacy inside the ipacd /tmp/MyAppunzip -o MyApp.ipa -d MyApp-unpacked > /dev/nullfind MyApp-unpacked -name "PrivacyInfo.xcprivacy"
The number of files listed here should be roughly the has-manifest count from Step 2 plus one for the app itself. If the count is off, a Pod is overwriting an old Privacy Manifest, or an Expo Config Plugin is stripping one in a later step.
Finally, run xcrun altool --validate-app to invoke the same automated check App Store Connect uses.
A No errors response means the submission almost always sails through. The peace of mind from passing validate-app is worth the few minutes it takes; I never skip it now.
Reconciling the Privacy Manifest with the Privacy Nutrition Label
It is worth pausing here to disentangle two things that are easy to confuse. The Privacy Manifest (PrivacyInfo.xcprivacy) is a machine-readable declaration of what your code touches. The Privacy Nutrition Label (App Privacy in App Store Connect) is a human-readable disclosure shown in the store. They should agree, and when they do not, reviewers will reach out individually.
A wallpaper app of mine ran into this last year. The manifest had NSPrivacyTracking: false, but in App Store Connect, "Identifiers — Device ID" was still marked as "Used for Tracking" from an old draft. The reviewer asked me to confirm the consistency. The reply itself took five minutes, but the round-trip ate twenty-four hours.
A reasonable pre-submission checklist is just these three lines. For each data type in App Store Connect's App Privacy section, verify that the "Used for Tracking," "Linked to User," and "Linked to Identity" flags do not contradict NSPrivacyTracking and NSPrivacyCollectedDataTypes in app.json. If you ship AdMob, confirm whether the design actually requests IDFA (request → true, no request → false). If you ship RevenueCat, decide whether the user identifier is anonymous (Linked to User: false) or email-linked (true).
I keep this mapping in _documents/privacy-mapping.md as a single table. Writing it once turns the next release's review into a five-minute confirmation.
Picking Required Reason API codes by intent, not by SDK
The reason codes in PrivacyInfo.xcprivacy (CA92.1, etc.) are defined per API category. Copying what an SDK ships works for that SDK, but if your own app code uses UserDefaults for settings storage, you must declare your own intent (CA92.1), separate from what the SDKs declare for theirs.
Here are the codes I reach for most often. The canonical list lives at https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_use_of_required_reason_api; always confirm against the upstream page.
NSPrivacyAccessedAPICategoryUserDefaults — CA92.1 for in-app settings storage, 1C8F.1 for shared defaults, AC6B.1 for CloudKit-linked usage
NSPrivacyAccessedAPICategoryFileTimestamp — C617.1 for app-managed caches and sync, 3B52.1 for displaying creation dates of user-selected files
NSPrivacyAccessedAPICategorySystemBootTime — 35F9.1 for elapsed-time UI updates, 8FFB.1 for measurement
NSPrivacyAccessedAPICategoryDiskSpace — E174.1 for in-app storage decisions, 85F4.1 for "do I have room to download" checks
I once forgot 35F9.1 on a wallpaper app and got a rejection routed through AdMob. AdMob's bundled manifest declared 35F9.1, but my own app also read SystemBootTime for a separate reason, and the app-level declaration was missing. Per Apple's rule that "the app declares intent independently of any SDK," keeping a small _documents/required-reason-mapping.md table reduces the chance of that mistake when adding a new SDK.
How far indie developers should automate this
These steps are achievable in half a day, but running them by hand every release does not scale. I have folded the following into a release-prep GitHub Actions workflow for both my wallpaper and mindfulness apps.
Before cutting the release tag, run audit-privacy-manifest.sh automatically
If any NO-MANIFEST row appears, post a Slack notification and pause for a human decision
If everything is has-manifest, proceed straight to EAS Build
When NO-MANIFEST rows appear, I dedicate thirty to sixty minutes to walk the Step 3 flow. With that in place, across five releases over the past year I have not had a single ITMS-91053 rejection.
The instinct to lean on automation goes back to when I first taught myself to code on the dial-up internet in 1997. What has changed is that Apple's policies get more detailed every year, and trying to keep them in human memory is no longer realistic. Especially in an era where AI tools like Rork generate much of the code, keeping a quiet rule on your side — "let the machines catch the bugs the machines can catch" — feels like the realistic way to keep an indie release cadence sustainable.
For your next release, try writing your own audit-privacy-manifest.sh. It takes about the same time as a single rejection cycle, and from then on releases get noticeably quieter.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.