DEV Community

Nabeel Hassan
Nabeel Hassan

Posted on Originally published at nabeelbaghoor.com

Most App Store Rejections Are Not About Your Code

Every engineer I know has the same first reaction to an App Store rejection: open the crash logs. That is almost always the wrong place to look. In my experience the build is fine. What failed is everything around the build: a login the reviewer could not get past, a form you filled in from memory, a calendar rule nobody on the team knew existed.

I managed App Store and Play Console releases as CTO at RAQTS, across mobile apps and a Unity platform, and I have been on the wrong end of enough review threads to stop treating store review as a final hurdle. Now I treat it like any other external system with an undocumented contract: probe it early, on purpose, and keep a checklist of the ways it fails.

Submit the first build as a test, not a launch

The most useful change I made was purely about timing. A rejection on launch day costs you a press date. A rejection two weeks earlier, on a build you submitted to see what happens, costs you nothing.

Both stores let you hold an approved build back. Apple lets you choose manual release after approval. Google Play lets you publish to an internal or closed track first. So I push a real build through review weeks early. A first app on a new account gets more scrutiny than an update, so I plan for one rejection and a resubmit and am pleasantly surprised when it does not happen.

The Google Play gate that is a calendar problem

If you create a new personal Google Play developer account, you cannot publish straight to production. You first run a closed test with at least 12 testers who stay opted in for 14 consecutive days, then apply for production access and answer questions about how you tested.

No engineering speeds that up. Discover it on launch week and you are two weeks late.

Two ways around it:

  • Use an organization account if there is a company. That needs its own verification (a D-U-N-S number and business details), which takes time, but it is not subject to the 12-tester rule in the same way.
  • Start the closed test the moment a usable build exists. With real testers, not twelve email addresses you control. Google asks what you learned from them, and that answer is easy to write when people actually used the app.

The general rule I took from this: developer accounts and signing keys get set up in week one, owned by the company, not by whichever contractor happened to create them.

The rejections I now plan for

1. The reviewer could not get in

If the app needs an account, give the reviewer one. Apple has a demo credentials field under App Review Information, and Google Play has an equivalent under App access.

The part engineers skip is the data. An empty account is technically a working login, but at RAQTS the product connected players and facilities, so a demo account with no facility, no sessions and no leaderboard history showed a reviewer a product that looked broken. The fix was a seeded demo account that exercised the main flows, plus review notes saying in two sentences what the app does and where the interesting part is.

I now think of the demo account as a fixture. It lives next to the other seed data, gets recreated by a script, and breaks the release checklist if it stops working. If a feature depends on hardware or a physical place, say so in the notes and attach a video, because the reviewer is not going to walk into a sports facility to test it.

2. It looks unfinished, or like a website in a box

Apple rejects apps that crash, show placeholder content, have dead buttons or link to "coming soon" pages. It also rejects apps whose main value is a wrapped website.

The engineering fix for the first part is boring: unfinished features go behind a flag, not behind a screen that says "soon". For the second, ask what the app does that the website cannot: push, offline, native camera or audio, widgets. If the honest answer is nothing, the reviewer has found something real.

3. No in-app account deletion

Both stores now require that if users can create an account, they can delete it. Apple wants deletion initiated inside the app. Google Play also wants a web link where people can request deletion without reinstalling.

"Email us to delete your account" does not pass. Build the real flow in version one: confirm, delete or anonymize server-side data, sign the user out. It is a day of work if you plan it, and an unpleasant week if you have to retrofit it into a schema where user IDs are foreign keys in fifteen tables.

4. Payments that skip the store

Digital content, features or subscriptions consumed inside the app default to the store's billing on both platforms. Physical goods and real-world services, like booking a court at a facility, are different and can use your own processor.

The rules have loosened in some regions and keep moving, so check current guidelines. What does not change is that the reviewer will tap every paywall, every "Upgrade" button and every link that leads to a checkout page. Decide the billing model before the first submission, because changing it later touches entitlement logic everywhere.

5. Privacy labels that do not match what the binary does

Apple's privacy labels and Google Play's Data safety form ask what you collect, why, and whether it is linked to the user. Those answers get compared against what the app and its SDKs actually do.

The mistake is filling the form in from memory of your own code. Your code is the small part. Analytics, crash reporting, attribution and ad SDKs all collect data on your behalf. So I generate the answer from the dependency list: every third-party SDK in the app, what each one collects, then the form. On Apple, also check that the privacy manifest covers any required-reason APIs your code or your dependencies use, or the upload gets flagged before a human ever sees it.

6. AI features without disclosure or controls

This is the category that has grown fastest, and it applies to most of what I build now. Apple's guidelines require apps to clearly disclose when personal data is shared with third parties, including third-party AI, and to get permission first. If the app sends someone's voice, notes or photos to an external model, say so in the flow where it happens, not only in the privacy policy.

Google Play's AI-generated content policy adds one more thing: users need a way to report offensive AI output from inside the app. A "Report this response" action wired to a backend log satisfies the intent, and it doubles as the cheapest eval dataset you will ever collect.

7. Store copy that overpromises

Screenshots must show the actual app. Descriptions cannot claim missing features, mention other platforms, or make health claims the app cannot back up. Anything near health or diagnostics should avoid "diagnose", "treat" and "cure" unless you have the regulatory standing for them.

After the first approval

Approval once is the start. For updates on a live product I lean on the stores' own safety rails:

  • Phased release on Apple and staged rollout on Google Play, so a bad build reaches a small slice of users first.
  • A minimum-version gate on the backend, because old builds stay in the wild for months. I wrote about that in The App Version You Shipped in March Is Still Calling Your API.
  • Builds from CI, not laptops, so signing and release do not depend on one person. My notes in Your Unity Pipeline Does Not Need Tests Yet apply almost unchanged to React Native and Flutter.
  • A calendar reminder for the target API level. Google raises the required Android target level every year, and an app that sits untouched eventually stops being offered to new users on newer devices.

At RAQTS the release pipeline was one of the first places I put my weight after stepping in as CTO, because a platform people use every day cannot afford a release that surprises anyone, the reviewer included.

The checklist I run before every first submission

  • Developer accounts created and owned by the company; Play closed test started early if required.
  • Seeded demo account recreated by script, two-sentence review notes, a video for hardware or location features.
  • No placeholder screens, dead buttons or "coming soon" links; unfinished work behind flags.
  • In-app account deletion, plus a web deletion link for Google Play.
  • Billing model decided for every paid feature.
  • Privacy labels and Data safety form generated from the real SDK list; privacy manifest checked.
  • AI data sharing disclosed in the flow, with consent and a way to report AI output.
  • Store copy and screenshots that match the build being submitted.

None of it is hard. It is just work that lives outside the codebase, which is exactly why engineers forget it.

What is the strangest rejection reason you have had from either store? I am collecting them for the next version of this list.


Originally published on nabeelbaghoor.com.

Top comments (1)

Collapse
 
beusebiu profile image
Eusebiu Balan •

The order inside "confirm, delete, sign the user out" bit me. My account deletion removed the user and then logged them out, and in Laravel the logout saves a fresh remember token on the user model. With the row already gone, that save turned into an INSERT and quietly wrote the account back.

Logging out first fixed it. Your foreign keys point was the other one: a table that cascades on user_id and also points at itself, which MySQL refuses to cascade, so some people couldn't delete their account at all.