◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-05-24Intermediate

Rolling Out Privacy Manifest to Six Wallpaper Apps in Parallel — Translating One Rejection into Five Clean Submissions

Operational notes from rolling out Privacy Manifest to six iOS wallpaper apps in parallel. How a single rejection on the first submission shaped the remaining five, and how phased release rings were designed for a UI-less update.

Privacy Manifest5Rork585wallpaper apps3parallel rolloutphased releaseindie dev30Claude in Chrome4

For two weeks after the Golden Week holiday, I spent my mornings on a quiet, slightly heavy task: rolling out a Privacy Manifest update across six iOS wallpaper apps. The first submission came back with a rejection, and the short memo I wrote to myself that afternoon ended up rescuing the remaining five. This article is the operational note from that two-week stretch. Rather than walking through the implementation details, I want to focus on the choreography of moving six apps in parallel, and on how the failure of the first one was translated into a checklist for the next five.

Implementation specifics on Required Reason API and SDK-chain auditing already live in two earlier posts on this site, so this one stays firmly on the operations side.

Why Run Six Apps in Parallel at All

I originally planned to ship the apps one by one. With Apple's review times being unpredictable that week, taking the risk on a single app before moving on felt safer. What changed my mind was realising that the actual Privacy Manifest content was almost identical across all six apps — AdMob, Firebase, RevenueCat, and MMKV were on the same versions, and the PrivacyInfo.xcprivacy payload was about 80 percent shared.

Both of my grandfathers were temple carpenters, and one of them used to say that "if the cuts are the same, cut them all at once before you start joining anything." If the preparation cost is roughly the same for one app as it is for six, then it makes sense to audit and fix in parallel, even if the actual submissions are staggered. I ended up with a hybrid approach: audit and fix all six together, but submit them in three waves of two apps each.

Three Lessons from the First Rejection

The first submission was rejected not with the usual ITMS-91053 error but with a note that the Privacy Nutrition Label disclosed on App Store Connect did not match the NSPrivacyTrackingDomains entries in the Manifest. The wording was polite, but the underlying issue was a mismatch rather than a missing declaration.

I took three concrete lessons away from this.

First, always look at the Privacy Manifest and the App Store Connect Nutrition Label as a pair. Cleaning up the Manifest alone is not enough — if the Connect-side checkboxes still reflect the older design, the submission comes back as a mismatch.

Second, the scope of NSPrivacyTrackingDomains is wider than I had been treating it. Ad-serving domains and A/B routing domains need to be inventoried alongside the analytics domains. In my case, the direct cause of the rejection was forgetting to declare googleads.g.doubleclick.net.

Third, the rejection text itself doubles as a checklist for the next submission. Apple's responses rarely spell out the reasoning, but the quoted line numbers and the SDK names mentioned in the body are usually enough to infer where to look in the next app.

How I Translated the First Failure into the Next Five

On the same day the first app was rejected, I wrote a one-page memo for myself. It contained:

  • The rejection reason in a single sentence
  • The exact declaration that was wrong and the diff that fixed it
  • A 5×3 checklist of places in the remaining five apps where the same symptom could appear
  • Five items on the Connect-side Nutrition Label that needed to be re-verified
  • Detection rules to fold back into the audit script later

I started calling this a "translation template." Not a per-app changelog, but a short procedure for applying one app's failure to the remaining N apps. The Manifest fixes on the next five apps each took about thirty minutes, and the Connect-side Label adjustments fifteen to twenty minutes per app. All five resubmissions passed on the first attempt.

Writing the memo took about forty minutes. If I had skipped it and handled the next five ad hoc, I am fairly sure I would have walked into a similar rejection somewhere around the third app.

Designing the Phased Release Rings

I considered putting all six apps on a maximum-duration Phased Release at the same time, but landed on a three-ring layout instead.

Ring 1 (two apps) holds the apps with lower install bases and the most direct user feedback channels. Phased Release is set to its shortest configuration, prioritising fast detection of regressions. Ring 2 (two apps) holds the mid-sized, revenue-critical apps; these are submitted only after Ring 1's first 24 hours come back clean. Ring 3 (two apps) holds the largest DAU apps, and is held back until Ring 2 has been quiet for 72 hours.

The ring design accounts for a quirk of wallpaper apps: a Privacy Manifest update has no visible UI change, which means there is little reason for users to manually relaunch the app. The propagation from release to actual device adoption is slower than a typical UI update, and the release calendar has to be designed around that delay.

Using Claude in Chrome to Watch Six Connect Pages

Manually checking six App Store Connect pages every morning was never going to last. Instead, I had Claude in Chrome cycle through six tabs each morning and extract three things:

  1. Apps whose review status changed in the last 24 hours
  2. New Crashlytics stack traces or sudden upticks in crash-free rate
  3. New user reviews containing the words "update," "ads," or "doesn't work"

The output came back as a short bullet list, and I only opened the tabs that actually needed my attention. Running six apps in parallel, the cognitive overhead of switching browser tabs alone is surprisingly draining; letting an AI do the first-pass filtering helped more than I expected. Having shipped indie apps since 2014 and accumulated roughly 50 million downloads across the catalogue, I have come to feel that letting an AI handle anomaly detection — rather than trying to eyeball every app every day — is what actually extends the lifespan of a portfolio.

What Six Parallel Submissions Actually Taught Me

Three takeaways to close on.

First, the essence of running things in parallel is not the fix itself, it is the translation of failure. If you can compress your first rejection into a one-page memo, the remaining N apps effectively become copies of a corrected release.

Second, do not assume that a UI-less Privacy Manifest update needs less Phased Release care. The slow device-side adoption actually argues for a more cautious ring design, not a looser one.

Third, running six apps as a single indie developer is not parallel work — it is parallel observation. Implementation stays sequential and human; only the watching is delegated to the AI. Once the layout settles into that shape, the cognitive load drops noticeably.

If you find yourself in a similar position next time, leave a blank page in your notes labelled "translation template" before you submit the first app. That single page tends to change how the second submission feels.

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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-05-23
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: enumerate every dependency, tell ITMS-91053 apart from ITMS-91061, and catch the Pods that npm names never show you — hermes included.
⬡ Dev Tools2026-05-18
Adapting Your Rork iOS App to iPhone Air and 17 Pro Series — Layout Patterns for 2026's New Resolutions
iPhone Air introduced a 420pt width that sits between the standard and Max models, quietly breaking layout conditionals in existing Rork apps. Based on updating four iOS apps at once, this covers device constant management, Safe Area handling, and full-screen image display fixes.
⬡ Dev Tools2026-10-15
When EXPO_PUBLIC_ Values Read Fine in Dev but Come Back undefined in Production: Telling the Spellings Apart
The API URL reads fine while developing and turns undefined once the app is built. The cause was how process.env was written. I lined up babel-preset-expo's output for dev and production and tabulated which spellings get replaced and which are left behind.