README is a short front page (about 120 lines) with one real visual per section; every former section moved unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth, studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools: tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
12 KiB
Catch rejections before you submit
App Store rejections cost days: you wait for review, read one sentence in the Resolution Center, fix it, and wait
again. On Google Play a policy problem can mean a blocked update, a removal or a strike on the account. Most of the
usual ones are visible in the project long before that. storebound scan reads the project itself, offline and
read-only (Xcode project, Info.plist and build settings, entitlements, PrivacyInfo.xcprivacy, Podfile.lock,
Package.resolved, Cartfile, Gradle files and version catalogs, AndroidManifest.xml (and the merged manifest of a local
build), res/xml and strings.xml, CMake / ndk-build files, Unity ProjectVersion.txt, package.json,
Expo app.json, capacitor.config, Cordova config.xml, pubspec, Unity Packages/manifest.json, ProjectSettings and
External Dependency Manager files, Godot export presets), plus your config and listing files. It works out the stack
and the SDKs (ads, analytics, attribution, crash, sign-in, payments, push, with versions and the file:line they come
from) and reports every rejection risk it can prove, each with the guideline, the evidence, the fix, a severity and the
skill to open. On the teaser fixture (tests/fixtures/scan/teaser):
$ storebound scan
Rejection risks: 2 blocker, 3 high, 0 medium, 0 low
1. BLOCKER 2.1(a) · App Store · Sign-in is required but there is no demo account for App Review
at storebound.config.ts:12 reviewAccess: { signInRequired: true, instructions: 'Sign in with the demo account.' },
at storebound.config.ts:11 accounts: { enabled: true },
at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0
2. BLOCKER 5.1.1(v) · App Store · Accounts can be created but no in-app account deletion was found
at storebound.config.ts:11 accounts: { enabled: true },
at Planner/LoginView.swift:11 Button("Create account") { Task { try await AccountApi.signUp() } }
at Planner/LoginView.swift:17 static func signUp() async throws {
3. HIGH 4.8 · App Store · Third-party login (Google / Facebook / ...) without Sign in with Apple or another equivalent login option
at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0
at Planner/LoginView.swift:9 GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController()) { _, _ in }
4. HIGH 3.1.1 · App Store · Restorable purchases (non-consumables or subscriptions) but no Restore Purchases path was found
at storebound.config.ts:16 com.example.planner.pro (non_consumable)
5. HIGH 5.1.2(i) · App Store · Tracking-capable code without App Tracking Transparency (Google AdMob, IDFA)
at Podfile.lock:2 Google AdMob: Google-Mobile-Ads-SDK 11.10.0
at Planner/Ads.swift:7 print(ASIdentifierManager.shared().advertisingIdentifier)
Every finding also has a fix: line and the skill. Below the risks comes the declarations proposal: the Play Data
safety and App Store App Privacy answers, App Tracking Transparency and privacy manifest entries the detected SDKs and
code imply, each marked detected (file:line) or ask owner, as a diff against dataSafety / appPrivacy in
the config. Nothing is written unless you run storebound scan --write (only detected answers; questions stay
questions). The MCP tool scan is read-only and cannot write; validate includes the open scan findings; the desktop
app shows them on the Checks tab. A finding you decided is fine goes into the config with its reason:
scan: { ignore: [{ rule: 'demo-account', reason: '...' }] }; a rule with several checks (e.g. play-metadata) takes match: '<text in the finding title>' to accept just one of them.
| rule | App Store | Google Play | what scan proves |
|---|---|---|---|
demo-account, play-app-access |
2.1(a) | App access | a sign-in (config, auth SDKs, sign-in code) without reviewAccess or without the demo account in .storebound/ |
account-deletion, play-deletion-url |
5.1.1(v) | User Data: account deletion | sign-up or social login without a deletion path in code or accounts.inApp; no accounts.deletionUrl |
login-services |
4.8 | Google / Facebook / other third-party login without Sign in with Apple or another equivalent option | |
restore-purchases, external-purchase, play-payments |
3.1.1 | Payments | non-consumables or subscriptions without a restore path; card / PayPal SDKs next to in-app products; "buy on our website" (Play: unless play.billingPrograms lists user choice billing or external offers) |
subscription-info |
3.1.2(c), 3.1.2 | no Terms of Use URL, no Terms link or renewal wording in the description, no legal links or "Manage subscription" in the app | |
att |
5.1.2(i) | tracking-capable SDKs or IDFA without ATT; the prompt without its purpose string; the string without a prompt | |
purpose-strings, unused-permission |
5.1.1(ii), 5.1.1(iv), ITMS-90683 | a used permission without its NS*UsageDescription; empty or vague strings; declared but unused | |
privacy-manifest |
ITMS-91053 / 91061 | no PrivacyInfo.xcprivacy, required-reason APIs not declared, SDKs older than their first manifest version | |
privacy-policy-link, play-privacy-policy |
5.1.1(i) | User Data: privacy policy | no privacy link in the app's sources (Play: every app; medium when it collects data), an http:// link |
privacy-labels |
5.1.2 | User Data: Data safety | SDK data types missing from dataSafety; Play also: an answer narrower than the SDK's disclosure (purposes, sharing), SDKs with no dataSafety at all |
play-prominent-disclosure |
User Data: prominent disclosure, Location permissions | background location without a disclosure that says "location" and "background"; location, contacts, calendar or phone number access without any disclosure or rationale text in sources or strings.xml |
|
play-background-location, play-photo-video, play-all-files-access, play-accessibility, play-install-packages, play-health-connect, play-full-screen-intent, play-vpn |
Location permissions, Photo and Video Permissions, All files access, AccessibilityService API, REQUEST_INSTALL_PACKAGES, Health Connect, Full-screen intent, VpnService | the restricted permission or service in the manifest (or the merged manifest of a local build), each with the core-use test and the Play Console declaration it needs; isAccessibilityTool lowers the accessibility finding to medium |
|
play-fgs, play-exact-alarm, play-query-all-packages, play-sms-call-log |
Foreground service types, exact alarms, package visibility, SMS and Call Log | foreground services without a type or its permission (and the Console declaration); USE_EXACT_ALARM; QUERY_ALL_PACKAGES; SMS and call log permissions | |
kids-ads, play-families-ads, play-families-ad-id, play-families-location |
1.3 | Families | a Kids Category age band with third-party ads or analytics SDKs; play.targetAges under 13 with an ad SDK that is not Families self-certified (or older than its certified version), the AD_ID permission in a child-only app (mixed audience: ad / attribution SDKs need child-directed treatment), location in a child-only app |
play-ad-id |
Advertising ID | targetSdk 33+ with ad / attribution SDKs and no AD_ID permission (medium when the merged manifest proves it is missing; Google Mobile Ads adds it itself) | |
play-16kb |
16 KB page sizes (updates blocked from 2027-02-01) | Unity before 6000.0.38f1 / 2022.3.56f1 / 2021.3.48f1, React Native before 0.77, NDK before r28 for the app's own C/C++ without the 16 KB flags, AGP before 8.5.1 with uncompressed native libraries; else prebuilt .so files to check |
|
play-ads-launch |
Ads, Better Ads Experiences | an interstitial shown from onCreate of the launcher activity (Android native; other engines: review-only) |
|
play-testing-requirement |
Testing for new personal accounts | play.accountType: 'personal' created after 2023-11-13 (12 testers, 14 days of closed testing before production); low when the account type is not set |
|
other-platforms |
2.3.10 | "Android", "Google Play" and other stores in the App Store text or screenshot captions | |
metadata-keywords, kids-terms, play-metadata |
2.3.7, 2.3.8 | Metadata | other apps' names, prices and "best / #1" in name, subtitle or keywords; keyword lists; "For Kids" outside the Kids Category. Play title over 30 characters, emoji, ALL CAPS, rank / price / promotion terms; other apps in the title or short description; repeated keywords or keyword lists in the full description |
play-impersonation |
Impersonation | a well-known brand in the package name or app name (low: a check, not a verdict) | |
placeholder-text |
2.1(a) | Metadata | lorem ipsum, TODO, template lines in the listings |
beta-wording |
2.2 | "beta", "demo", "trial version", "test build", "early access" in the app name (config, CFBundleDisplayName) or the App Store name, subtitle, keywords, promotional text and description ("demo account" is fine) | |
originality (with --originality) |
4.3 | Repetitive content | a near-duplicate of one of your own apps (storebound originality scores: name, listing text, icon and screenshot hashes, project structure) |
min-functionality |
4.2 | Capacitor server.url / Cordova <content src> on a remote site; a few native files around a remote WKWebView |
|
deprecated-api, private-api |
2.5.1 (ITMS-90809) | UIWebView; App-Prefs: / prefs:root URL schemes |
|
ipv6, ats |
2.5.5, App Transport Security | IPv4 literals in server URLs; NSAllowsArbitraryLoads | |
export-compliance |
export compliance | no ITSAppUsesNonExemptEncryption and no apple.usesNonExemptEncryption, or the two disagree (never guessed) |
|
review-prompt |
5.6.1, 3.2.2(x) / 5.6.3 | a custom "rate us" prompt without the system API; rewards for ratings | |
play-target-sdk |
Target API level | below API 36 (Wear OS / Automotive 35, TV / XR 34) for new apps and updates since 2026-08-31 | |
play-package-name |
Play Console upload | the reserved com.example prefix |
storebound scan --originality [--against <folders>] adds the Guideline 4.3 / Play repetitive content check against
your other apps to the same report (folders default to originality.against in the config; storebound originality
stays the command with every score and suggestion).
Rules cite Apple's App Review Guidelines and the Play Policy Center / Play Console Help pages they come from (checked
2026-10-11, in the code comments of cli/lib/scan/rules.ts and cli/lib/scan/play-rules.ts); the SDK table with each
vendor's data disclosure page and Google's Families self-certified ads SDK list is cli/lib/scan/sdks.ts. Play config
the rules read: play.targetAges (Target audience age groups), play.accountType / play.accountCreated, and
play.billingPrograms. When the project was built locally, scan also reads the merged manifest under
build/intermediates (the permissions libraries add); otherwise it says that it could not. Not a rule because no Play
policy page covers it: device admin apps (BIND_DEVICE_ADMIN); not a policy at all: Play Integrity / anti-tamper.
Checked at review, not by scan (no file shows them): crashes and hangs on the reviewer's device (2.1, store-health),
links that are dead at review time (validate checks the configured URLs), screenshots and descriptions that do not
match the app (2.3, store-screenshots), unfinished UI (4.0), "too simple" and spam / template apps (4.2, 4.3,
portfolio-originality), IAPs that fail in the sandbox (2.1(b), store-iap-setup), the content of the privacy policy
(privacy-policy), local law (5.2). On Google Play: whether a restricted permission really serves the app's core
purpose (the Console declaration and its video decide), the wording and timing of a prominent disclosure, ad
placements in Unity, Flutter or React Native code, the Health apps declaration answers, and the alignment inside a
prebuilt .so (scan lists them; zipalign -c -P 16 and check_elf_alignment.sh check the built app). The
review-rejections skill covers what to do when one of these comes back.