WizusLabs Engineering · Studio

The permission you don’t use is a liability, not a convenience

We told Apple one of our games plays audio in the background. The game does the exact opposite — it goes silent the instant you leave it. Apple noticed, and rejected the build. Then, a year later, we did it again in a different game.

By WizusLabs Engineering · 2026-07-23 · ~7 min read

There is a tempting way to think about an app capability: it is a door you might want open later, so you leave it unlocked now, just in case. That instinct is exactly backwards. On iOS, every capability you declare is not a convenience you have banked — it is a claim you have made about what your app does, and a reviewer will hold you to it. Declare background audio and you have promised, in a config file, that your app keeps playing sound after the user swipes away. If your app then does the opposite, you have not left a door open for later; you have written down a lie about yourself and handed it to the one reader who checks. We know because we did precisely that — and then, having fixed it, did it again.

A promise you make in a config file

The mechanism is small and unglamorous. An iOS app ships a property list, Info.plist, that among other things can carry a key called UIBackgroundModes — an array of the specific things the app is allowed to keep doing while it is not on screen: playing audio, tracking location, handling voice-over-IP, finishing a download. The audio mode is the one that matters here. It exists for the apps that genuinely need it: music players, podcast clients, streaming services — software whose entire job is to keep sound going while you do something else. Declaring it tells the operating system, and Apple’s reviewer, “this app is one of those.”

Our games are not one of those. A puzzle game and an arcade game have no reason to make noise once you have left them; the respectful, battery-friendly thing — and the thing our code actually does — is to cut all audio the moment focus is lost. In Flutter that is a handful of lines: a WidgetsBindingObserver listens for didChangeAppLifecycleState, and the instant the state goes to paused, we pause every sound. That behaviour is deliberate and we are proud of it — it is the whole subject of an earlier devlog on why a well-behaved game goes silent the moment you leave it. Which is what makes the UIBackgroundModes declaration so absurd in hindsight: the capability we had asked for was one our own code goes out of its way to refuse to use.

A declared capability contradicted by the app’s own behaviour On the left, the Info.plist declares UIBackgroundModes containing audio — a promise that the app keeps playing sound while backgrounded. On the right, the app's own lifecycle code responds to didChangeAppLifecycleState by pausing all audio the instant focus is lost — the app goes silent. The two do not agree. App Store review reads the gap between the claim and the behaviour and rejects the build under Guideline 2.5.4. What the manifest declares UIBackgroundModes ↳ audio “I keep playing sound while backgrounded.” What the app actually does didChangeAppLifecycleState ↳ paused → pauseAll() “I go silent the instant you leave.” App Store review reads the gap rejected · Guideline 2.5.4 a capability declared but never used
The declaration is a promise; the code is the truth. When they disagree, the reviewer believes the code — and the unused capability becomes the reason for the rejection.

We shipped the same mistake twice

Here is the part that is not flattering, told straight. On 2026-05-22 our Sudoku game was rejected under App Store Review Guideline 2.5.4 — the rule that an app must not declare background modes it does not actually use. The cause was exactly the UIBackgroundModes: [audio] entry described above, sitting in an Info.plist for an app that pauses its audio on background. We removed the key, resubmitted, and the app went through. Problem solved, we thought. It was not, because we solved the wrong problem.

We fixed the file and never wrote down the lesson. There was no note in a shared place that said “a game must never declare background audio,” no check that would catch it, nothing but the memory of the one person who happened to deal with the rejection. Memory is not a control. So on 2026-07-09 our tank game — a completely separate app, built and configured on its own timeline — was rejected under the identical guideline for the identical reason: the same audio background mode, declared in a game that also goes silent when you leave it. Two apps, one defect, two rejections, roughly seven weeks apart. The second one stings more than the first, because the first was a mistake and the second was a mistake we had already been warned about and let happen anyway.

The fix was deletion, not configuration

The immediate fix is almost anticlimactic: you delete the key. A default Flutter app ships without any UIBackgroundModes entry at all, which is correct, because the default app has no background feature. The risk is never that the key is missing and should be there; it is always that someone added it — copied from a tutorial, inherited from a template, pasted from a Stack Overflow answer meant for a media app — and it quietly rode along. An empty <array/> under the key is no better, by the way: it still draws a reviewer’s eye and invites the same question. The right state is not “declared and empty.” It is “not declared.”

This is the principle of least privilege wearing ordinary clothes. Security people say it about access rights: grant the narrowest set of permissions the job actually needs, because every extra grant is attack surface you now have to defend. A store capability is the same shape of thing. Every mode you declare is a promise you have to keep, a behaviour a reviewer can test you against, and a line of divergence between what your manifest says and what your binary does. The capability you are not using is not sitting there harmlessly — it is the single most likely thing to get your next release bounced, precisely because nobody remembers it is there.

The real bug was in our memory, so we fixed that

Deleting the key fixes one build. It does nothing to stop a third app, a year from now, from reintroducing the exact same entry — and the two-time recurrence is the actual story here, not the plist. A fix that lives only in one engineer’s head is not a fix; it is a countdown. So the durable change was not in any app’s code. We wrote a small audit script that scans every app’s Info.plist and fails loudly if it finds a UIBackgroundModes entry — including an empty one — that is not accompanied by an explicit, written waiver. It runs before any iOS build and as part of release clearance, so a rogue capability cannot reach a reviewer without a human first having typed out, on the line above it, exactly which background feature justifies it and why.

The waiver mechanism matters as much as the block. We are not banning background audio forever; some future app might genuinely stream sound and legitimately need the mode. What we are banning is the silent declaration — the capability nobody chose on purpose. To keep the mode now, you have to leave a marker comment stating the real feature and be ready to show it, which turns an accidental liability into a deliberate, documented decision. That is the difference between a rule and a habit: a habit is what one person remembers, and a rule is what the build refuses to skip. We had relied on a habit twice and it failed twice; the third time, the machine checks. The same instinct runs through our automated QA gate — the things you cannot afford to forget should not depend on remembering them.

If you want to see the behaviour this whole episode was contradicting — the game cutting its own sound the instant you look away — it is running in Iron Swarm and in our Sudoku app, both of which now declare exactly the capabilities they use and not one more. That is the quiet payoff: the apps do less on paper than they used to, and are more honest for it.

The lesson generalises past this one key. A manifest, an entitlement, a scope on a token, a permission in a settings screen — every one of them is a claim you are making about your own software, and the ones you are not using are the ones that will surprise you, because you have forgotten you are still making them. Declare what you use. Delete what you don’t. And when a fix depends on a person remembering, the fix is not finished — the longer version of how the small disciplines behind a shipped product compound is on the WizusLabs Engineering blog. A permission you do not use is not a convenience you have saved; it is a debt you have forgotten you owe.

Notes

This is a first-hand engineering account of two App Store rejections we caused and the change we made in response. It is iOS-specific by nature — the mechanism is Apple’s Info.plist, the UIBackgroundModes key, and App Store review — and nothing here is a claim about any other platform. It deliberately carries no invented metrics: the only figures in the piece are two rejection dates from our own records (Sudoku, 2026-05-22; the tank game, 2026-07-09) and the fact that it happened twice. We have not attached a review-time, revenue, or download number to any of it, because we did not measure those and manufacturing one would undercut the point. The specifics of what App Store review requires can change over time; the guideline text and the property-list documentation we relied on are cited below, and you should verify current wording against Apple’s own pages before relying on it.

Sources

The Apple requirements this post leans on are documented by Apple (living documents; accessed 2026-07-23):

Keep reading: all posts on the WizusLabs Engineering blog.

← Back to the Blog