
# storebound
**Website: [storebound.dev](https://storebound.dev) · npm: [storebound](https://www.npmjs.com/package/storebound) · `bunx storebound init`**
[](https://www.npmjs.com/package/storebound)
[](LICENSE)
[](CHANGELOG.md)
[](https://bun.sh)
[](package.json)
[](skills)
[](#mcp-server)
**Take a mobile app or game from nothing to "Submit for review" on Google Play and the App Store: app records,
listings in every language, in-app products and subscriptions with regional prices, every questionnaire, account
deletion and review access, privacy, icons, screenshots and previews, with one config and your AI coding agent.**
Hey, I'm CRTkafa. I built this while getting my own game through both stores and hit every wall on the way: draft
apps, edits that die when you touch the Console, the "first IAP must ship with a version" dance, Data safety CSVs that
silently need one more answer, an IARC questionnaire, target audience, App Privacy labels that are saved but not
published, screenshots in 7 sizes x 14 languages... I wrote each fix down as code or as a checklist. None of those
walls is game-only: a to-do app with accounts and a subscription hits the same ones, plus a few of its own (a demo
account for the reviewer, account deletion, subscription disclosures), so storebound covers apps and games alike. It
works for me, so I open-sourced it. Use it too.
No account, no sign-up, no telemetry. A small Bun CLI that talks to the official store APIs,
Agent Skills that walk your AI coding tool (or you) through the parts that are still console clicks, and an MCP server
so agents can call the CLI safely. **storebound never presses Submit, Release or Publish for you.**
Pick a profile and start from honest templates: `init --profile app` (utility / productivity app, optional account,
analytics, no ads; the default), `--profile subscription-app` (accounts + auto-renewable subscriptions) or
`--profile game` (free-to-play with ads and IAP).
It keeps going after launch: TestFlight groups and testers, crash and ANR vitals against the store thresholds, review
replies you approve before they go out, prices for many products and regions from one CSV, store analytics next to
your listing changes, and App Store product page tests from your screenshot variants.

## The journey
Everything an app or game needs before review, in the order I do it. For each step: what storebound does through the store
APIs (or on your machine), what the skill walks your agent through where the stores have no public API, and what stays
your own click.
- **storebound**: a CLI command (also an MCP tool). Every write has `--dry`; MCP write tools default to a dry run.
- **skill + agent (UI)**: no public API exists for it. The named skill has the exact navigation and the prepared
answers. An agent with browser tools (a Chrome extension, computer use, a Playwright MCP ...) can click through it
while you watch; without one, you click with the skill as your checklist.
- **you**: public, legal or money steps. The skills tell every agent to stop and hand these to you.
| # | step | storebound (API / local) | skill + agent (UI) | you |
|---|---|---|---|---|
| 1 | **Set up** | `init --profile app\|game\|subscription-app` (config, `store/*.md`, declarations, App Privacy and privacy templates per profile), `install --agent `, `check` | Play: service account + API access invite; App Store: API team key (`play-first-release`, `appstore-first-release`) | developer accounts; Paid Applications agreement, banking and tax; Play payments profile |
| 2 | **App records and bundle id** | `apple bundle` registers the bundle id, `apple status` finds the app record | App Store Connect > New App (no API); Play Console > Create app; the very first AAB upload when Play needs it from the Console | the declarations you accept when creating the Play app |
| 3 | **Builds and testers** | `play next-version-code`, `play build`, `play upload --track internal`, `apple signing ensure`, `apple build`, `apple upload` (waits for processing), `apple encryption`, `apple testflight groups / what-to-test / info / distribute` (internal groups) | Play internal testers list, Apple sandbox testers (`play-first-release`, `testflight`) | `apple testflight testers` (Apple emails the invitations), external groups and Beta App Review (`distribute --submit-review`) |
| 4 | **In-app products and subscriptions** | `play products` (regional prices from USD + your overrides, activated), `apple iap` (consumable, non-consumable, non-renewing; localizations, price schedule, availability), `apple iap-review`, `apple subscriptions` (groups, levels, equalized prices, intro and promotional offers, offer codes), `play subscriptions` (base plans, regional prices, offers), `catalog diff` (both stores against one catalog) | Play license testers, Apple sandbox testers, "Add for Review" of the first IAPs and subscriptions into the version's submission, Play promo codes (`store-iap-setup`, `store-subscriptions`) | custom offer codes (`apple subscriptions --codes` asks first) |
| 5 | **Store listings, every language** | `play listing` (title, short / full description, contact email + website, promo video URL), `apple listing --version x --build N` (name, subtitle, description, keywords, promo text, URLs, privacy URL, manual release), `listing translate` (drafts per language), `whats-new`, `check` (limits), `check --aso` | Play store settings: category and tags (`play-first-release`) | |
| 6 | **Play questionnaires** | `play data-safety`: the whole Data safety form from `dataSafety` in the config (CSV import through the API) | Policy and programs > App content, answers prepared in `store/declarations.md` (`store-declarations`): privacy policy, app access with the demo account (`review-access`), delete-account URL (`account-deletion`), ads, content rating (IARC), target audience and content, advertising ID, news, government, financial and health features; Data safety summary check | the answers themselves are your statements: the agent asks when the code does not prove them |
| 7 | **App Store questionnaires** | `apple appinfo` (categories, age rating declaration, content rights, Free price, availability), `apple encryption` (export compliance from `apple.usesNonExemptEncryption`, never guessed), `apple review-details` (contact, demo account from `.storebound/`, notes) | App Privacy nutrition labels from `store/app-privacy.md`, then **Publish** (`store-declarations`); encryption documentation when the answer is yes (`export-compliance`); the first availability record if the API refuses it | the export compliance answer (a legal statement, set once in the config) |
| 8 | **Privacy and accounts** | privacy policy page template (`init`), `privacy-manifest --ios ` (PrivacyInfo.xcprivacy, required-reason APIs, SDK manifests), `validate` checks the policy, delete-account and terms URLs answer | host the pages, link them in both consoles and inside the app; in-app account deletion (`privacy-policy`, `privacy-manifest`, `account-deletion`) | |
| 9 | **Icons** | `icons`: App Store 1024 without alpha, Play 512, adaptive + legacy Android launcher icons, web icons, Capacitor sources | | |
| 10 | **Screenshots and previews** | `shots capture / render / check / gallery`, `play images`, `apple screenshots`, `shots record / video`, `apple previews`, `apple ppo create / upload` (App Store product page tests from `shots` variants; see [Store art](#store-art)) | Play store listing experiments (optional, `store-screenshots`) | uploading the promo video to YouTube; `apple ppo start / stop` |
| 11 | **Validate** | `scan` (offline: rejection risks in the project files, see [Catch rejections before you submit](#catch-rejections-before-you-submit)), `check` (offline) and `validate` (read-only on both stores: every blocker in fix order, plus the local renders) | the UI-only items `validate` cannot see (`store-preflight` lists them) | |
| 12 | **Submit** | `play release --version-code N --production draft`, `apple status` | Play: countries / regions, open the production draft and save it (`play-first-release`) | **Play: Send changes for review. App Store: Submit for Review.** |
| 13 | **After review** | `apple status --wait-review`, `apple release --phased on / pause`, `play rollout --halt`, reply drafts (`review-rejections`) | | releasing the approved version, resuming or completing a phased release or rollout (terminal commands that ask first, `release-management`) |
| 14 | **Live app** | `health [--weekly]` (crash / ANR vitals vs Play's thresholds, App Store diagnostics; also in `validate`), `reviews list / draft`, `analytics snapshot / compare`, `prices export`, `prices import --dry`, `apple ppo status` | `store-health`, `review-replies`, `store-analytics`; Play statistics come from the reports bucket (no API) | `reviews send` (approved drafts only), `prices import` (asks first) |
New personal Play developer accounts also need a closed test with 12+ testers for 14 days before production; the
`play-first-release` skill covers it.
### API or console, honestly
- **Through the API** (storebound does it): bundle id, builds on tracks, versions and build selection, listings in
every language, graphics, screenshots, app previews, in-app products and prices (also from one CSV), Play Data
safety, Apple age rating, categories, content rights, price and availability, export compliance answer, App Review
details with the demo account, subscriptions (Apple groups, levels, prices, introductory and promotional offers,
offer codes; Play base plans, regional prices, offers), phased release and staged rollout, TestFlight groups / testers / What to Test / Test Information / build
processing / expiring builds, crash and ANR vitals and App Store diagnostics, TestFlight feedback, review replies,
App Store analytics reports, sales and finance reports, App Store Product Page Optimization tests, IAP and
subscription review submissions, offer codes for one-time purchases, win-back offers, promoted IAPs, pre-orders,
custom product pages and in-app events, and pulling live listing texts back.
- **Console only** (no public API; the skill + your agent's browser, or you): creating the App Store Connect app record
and the Play app, Play App content (IARC content rating, target audience, ads, advertising ID, app access,
government / financial / health / news, app access with the demo account), Play category and tags, Play countries,
App Privacy labels and their Publish, Play promo codes, Play pre-registration, Play store listing experiments, test results of both stores' experiments, promotional images of promoted IAPs.
Play store statistics have no API either: storebound reads the monthly exports from your Play reports bucket.
- **Only you**: agreements, banking and tax, and the final Send for review / Submit for Review (apps, IAPs, custom
product pages, in-app events). Releases and rollouts after approval, Beta App Review, tester invitations, sending
review replies, applying price changes, starting / stopping a product page test, creating offer codes, win-back
offers, switching on promoted IAPs, pre-orders and expiring builds too. None of these is an MCP tool; the terminal commands ask first.
## 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`):
```text
$ 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: ''` 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 `` 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 ]` 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.
## 5-minute quick start
You need [Bun](https://bun.sh) 1.1+ (and `ffmpeg` for icons, screenshots and video).
```sh
cd my-app
bun add -d storebound playwright # or: bun add -d git+https://git.crt.fyi/CRT/storebound.git playwright
bunx storebound init # --profile app (default) | subscription-app | game: config, store/*.md, declarations, privacy policy, .storebound/ (gitignored)
# fill in storebound.config.ts and the store/ files
bunx storebound check # offline: config, copy limits, Data safety answers, products, files
```
Add credentials (below), then walk the journey with dry runs first:
```sh
bunx storebound apple bundle --dry
bunx storebound play listing --dry
bunx storebound play products --dry && bunx storebound apple iap --dry
bunx storebound apple subscriptions --dry && bunx storebound play subscriptions --dry
bunx storebound apple review-details --dry # contact + demo account from .storebound/ (masked) + notes
bunx storebound play data-safety --dry --csv store/data-safety.csv
bunx storebound apple appinfo --dry
bunx storebound icons --dry
bunx storebound shots capture && bunx storebound shots render && bunx storebound shots gallery
bunx storebound validate # read-only: what still blocks a submission, per store, in fix order
bunx storebound catalog diff # read-only: products + subscriptions on both stores vs the config, prices per region
```
After launch:
```sh
bunx storebound apple testflight what-to-test --build 42 --dry # then distribute --build 42 --group Team
bunx storebound health --weekly # vitals + diagnostics, .storebound/health/.md
bunx storebound reviews draft # reply drafts in .storebound/replies/, then reviews send
bunx storebound prices export && bunx storebound prices import --dry
bunx storebound analytics snapshot && bunx storebound analytics compare
```
Or ask your agent: "take my app to Google Play and the App Store", "fill in the store questionnaires", "set up the
in-app purchases and a yearly subscription with a free trial on both stores", "do both stores sell the same
catalog", "the reviewer could not sign in", "make store screenshots in 5 languages", "is my app ready to submit", "why was my
update rejected", "put build 42 on TestFlight", "how are our crash rates", "answer the new reviews", "did the new
screenshots help". The matching skill takes over.
### Coming from fastlane
`storebound import fastlane` turns an existing fastlane setup into storebound files. It only reads (no Ruby is run),
never overwrites anything and prints what it could not map:
```sh
bunx storebound import fastlane --dry # the plan + report; default --dir: fastlane/, ios/fastlane/, android/fastlane/
bunx storebound import fastlane # writes the files below; an existing file gets a .imported. copy
bunx storebound init # adds the remaining templates, keeps the imported files
bunx storebound check
```
| fastlane | storebound |
|---|---|
| Appfile `app_identifier`, `package_name` | `apple.bundleId`, `play.packageName` |
| deliver `metadata//*.txt` (+ `default/`) | `store/appstore.md` per App Store locale; URLs, copyright, categories and review notes in the config |
| supply `metadata/android//*.txt`, newest `changelogs/.txt` | `store/listing.md` per Play language |
| deliver and supply language folders | `languages[]` with both store codes (`tr` + `tr-TR`, `zh-Hans` + `zh-CN`, `he` + `iw-IL`) |
| `screenshots//`, `images/phoneScreenshots/`, `tenInchScreenshots/` | `store/shots/frames///NN.png` with a `folder` capture source |
| supply `icon`, `featureGraphic` | `play.icon`, `store/renders//feature/1024x500.*` |
Keys and accounts are never copied: `json_key_file`, the API key, team ids, the App Review contact and demo account are
named in the report with where they go (`.storebound/credentials.json`). Submission and release flags, `price_tier`,
the age rating config, lanes and signing (`match`) are listed with their storebound counterpart. The
`migrate-from-fastlane` skill walks an agent through it.
### Credentials (never in the repo)
Read from environment variables, or from `.storebound/credentials.json` in your project (gitignored by `init`):
| name | what |
|---|---|
| `PLAY_SERVICE_ACCOUNT` | path to the Google Cloud service account JSON key (or the JSON itself) with access to your app in Play Console |
| `ASC_KEY_ID`, `ASC_ISSUER_ID` | App Store Connect API team key (Users and Access > Integrations) |
| `ASC_P8` | path to `AuthKey_.p8` (default `~/.appstoreconnect/private_keys/AuthKey_.p8`) |
| `ASC_VENDOR_NUMBER` | vendor number for `reports apple-sales` / `apple-finance` (Payments and Financial Reports) |
| `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | upload key for `play build` (path to the .jks / .p12 and its passwords; handed to Gradle through the environment) |
| `reviewContact` | (file only) App Review contact name / phone / email |
| `reviewDemoAccount` / `REVIEW_DEMO_USER`, `REVIEW_DEMO_PASSWORD` | demo account for App Review (`reviewAccess.signInRequired`); never in the config or the repo, masked in logs |
Several store accounts on one machine: credential profiles (`~/.storebound/credentials/.json`, the same names
except the per-app upload keystore)
picked with `--account ` or by a workspace ([Many apps and studios](#many-apps-and-studios)).
## Install for your AI tool
Every route gives the agent the same skills and the same MCP tools. Nothing here requires Claude: the skills are
plain Markdown and the CLI is a normal command.
| tool | install |
|---|---|
| Claude Code | `/plugin marketplace add https://git.crt.fyi/CRT/storebound.git` then `/plugin install storebound@crtkafa` (skills, MCP server, `storebound` on PATH). Or per project: `bunx storebound install --agent claude` |
| OpenAI Codex | `bunx storebound install --agent codex` (skills in `.agents/skills`, MCP in `.codex/config.toml`; Codex reads that file for trusted projects). The repo is also a Codex plugin (`.codex-plugin/plugin.json`, skills only) |
| Gemini CLI | `gemini extensions install https://git.crt.fyi/CRT/storebound.git` (skills, MCP server, `GEMINI.md`). Or `bunx storebound install --agent gemini` |
| Cursor | `bunx storebound install --agent cursor` (skills in `.agents/skills`, `.cursor/mcp.json`); the repo's `.cursor/rules/storebound.mdc` is an agent-requested rule you can copy |
| VS Code / GitHub Copilot | `bunx storebound install --agent copilot` (skills in `.github/skills`, `.vscode/mcp.json`) |
| Windsurf (Devin Desktop) | `bunx storebound install --agent windsurf` (skills in `.agents/skills`, MCP in your global `mcp_config.json`) |
| OpenCode | `bunx storebound install --agent opencode` (skills in `.opencode/skills`, `opencode.json`) |
| anything that reads `AGENTS.md` | copy or link `AGENTS.md` (rules + skill index) and use the CLI |
`install` takes several agents at once (`--agent codex,cursor`), `--dry` to preview, `--no-skills` / `--no-mcp`,
and `--command ""` to choose how the MCP server starts. It only adds or updates the `storebound` entry and its
own skill folders; everything else in those files stays as it is, and re-running it is safe.
## Commands
Every write command takes `--dry`; re-runs are idempotent; output is one timestamped line per step, so long jobs can
run in the background with a log (`storebound shots render > .storebound/render.log 2>&1 &`).
Setup and checks
| command | kind | what |
|---|---|---|
| `init [--profile app\|game\|subscription-app]` | local | copy the templates of a profile (default app), gitignore `.storebound/` |
| `import fastlane [--dir fastlane] [--profile ...]` | local | config, `store/appstore.md`, `store/listing.md` and screenshot frames from a fastlane setup; never overwrites, reports what is not mapped |
| `install --agent ` | local | skills + MCP entry for an AI tool |
| `mcp` | local | stdio MCP server wrapping the commands |
| `serve --stdio` | local | JSON-RPC session for the storebound desktop app (see [Desktop machine interface](#desktop-machine-interface)) |
| `inspect [--json]` | local | offline summary of the config: name, store ids, languages, products |
| `check` | local | config, copy limits, data safety, products, subscriptions, profile (delete-account URL, demo account, terms URL), files; warns on untranslated release notes |
| `catalog diff [--store] [--threshold 10]` | API, read-only | one catalog vs both stores: missing / extra / mismatched products and subscriptions, US and override prices, locales, offers, per-region price differences between the stores; exit 1 on mismatches |
| `check --aso` | local | keyword audit: App Store keyword field rules, Play stuffing / claims |
| `scan [--store apple\|play] [--json] [--write] [--originality --against ]` | local, read-only | rejection risks in the project itself (stack, SDKs, Info.plist, manifest, listings) with guideline, evidence file:line, fix and skill; Data safety / App Privacy / ATT / privacy manifest proposal as a diff (`--write` adds the detected answers to the config); exit 1 on blockers |
| `validate [--apple] [--play]` | API, read-only | readiness report with ordered blockers plus the local renders checks (`--no-local` skips); exit 1 while any blocker is left |
| `originality --against [--json]` | local, read-only | Guideline 4.3 check vs your other apps: name, listing text, icon and screenshot perceptual hashes, project structure; scores, near-duplicate flags, suggestions; exit 1 when flagged |
| `whats-new [--since ]` | local | draft player-facing release notes from git log into both listing files |
| `whats-new --languages en,de,tr` / `check` / `apply` | local | one file per language (`store/whats-new/`) for your agent (or a `--translate` hook of yours) to translate, per-store limits (Play 500, App Store 4000), then into both listing files |
| `listing translate --to de,ja [--from en] [--translate ""]` | local | per-language listing sections in both listing files (App Store name, subtitle, promotional text, keywords, description; Play title, short, full) for your agent (or a hook) to translate, marked as drafts until reviewed; keywords cleaned up |
| `listing check` / `listing reviewed --lang ` | local | every language section against the store limits and keyword rules, untranslated and unreviewed drafts (exit 1 on a problem) / remove the draft marker after a human read it |
| `flow list \| validate \| run [NAME=value] [--dry \| --apply] [--from \| --resume]` | local + whatever the steps call | repo-local workflows (`store/workflows/*.yml`): steps in order with progress; writes run as dry runs unless `--apply`, owner steps are listed as your click and never run (see [Repo-local workflows](#repo-local-workflows)) |
| `catalog pull [--write]` | API, read-only + local | IAP and subscription texts on the stores vs the config: a proposal diff (`.storebound/pull/catalog.json`); the config is never rewritten |
| `ads scan --ios ` | local | AdAttributionKit / SKAdNetwork: ad network ids, postback copy endpoints, attribution windows and cooldown, conversion value updates; exit 1 on a blocker |
| `privacy-manifest --ios ` | local | PrivacyInfo.xcprivacy, required-reason APIs, SDKs without manifests |
| `api list \| coverage \| apple\|play ` | API | every App Store Connect / Google Play operation (see [Every endpoint](#every-endpoint)); GET runs, writes are dry unless `--yes` in a terminal |
| `desktop [--download]` | local | open the storebound desktop app (in an app folder: add or select it there); not installed: the download for this system, `--download` saves it to ~/Downloads after checking its signature (never installs) |
| `doctor [--json]` | local | which local tools are here (ffmpeg, Chrome, playwright, Xcode, adb, git) and how to install the missing ones |
| `icons` | local | App Store 1024 (no alpha), Play 512, web, Capacitor sources, Android legacy launcher icons |
Google Play
| command | kind | what |
|---|---|---|
| `play listing [--include-drafts]` | API | app details, title / short / full description per language, promo video URL (`play.video`); prints the plan first (old -> new per field, draft languages skipped), `--dry` stops there, the real run asks and sends only changes |
| `play listing pull [--write]` | API, read-only + local | live listings back into `store/listing.md`: diff first, writes only with `--write` |
| `play images [--variant b]` | API | icon, feature graphic, phone / 7" / 10" screenshots, one edit per language |
| `play products` | API | one-time products: regional prices from `convertRegionPrices` + your overrides, activate |
| `play subscriptions [--csv]` | API | subscriptions: listings + benefits, one auto-renewing base plan, regional prices, intro and extra offers, activate (plan first, asks) |
| `play data-safety` | API | Data safety form (CSV import); `--csv` writes the file |
| `play build [--version-code N] [--flavor f]` | local + API read | Gradle `bundleRelease` (Flutter `flutter build appbundle`; Capacitor, React Native, Unity export found) with the next free versionCode and the upload key from env / `.storebound` (passed through the environment, never argv); AAB checked and copied to `build/android/` |
| `play upload [] --track internal\|alpha\|beta` | API | AAB to a testing track with release notes; versionCode checked first; `--dry` is read-only, a real run asks |
| `play release ` | API | upload + tracks (`--internal completed --production draft`, `--production inProgress --fraction 0.05`), release notes |
| `play rollout` | API | `--fraction x`, `--halt`, `--resume`, `--complete` (asks first, except halt) |
| `play next-version-code` | API, read-only | next free versionCode |
| `play status` | API, read-only | tracks, listings, products |
App Store
| command | kind | what |
|---|---|---|
| `apple bundle` | API | register the bundle id, find the app record |
| `apple listing --version x.y.z [--build N] [--include-drafts]` | API | prints the plan first (version: keep / update / create / refuse with the reason; per locale create / update, old -> new per field; draft locales skipped), `--dry` stops there, the real run asks and applies exactly it: version (manual release), the build with exactly that number, texts per locale (name: the locale's `### name`, else `apple.name`, else `app.name`) |
| `apple listing pull [--version x.y.z] [--write]` | API, read-only + local | live texts back into `store/appstore.md`: diff first, writes only with `--write` |
| `apple screenshots [--variant b]` | API | 6.9" + 6.5" (+ 13" iPad with `apple.ipad`, + 6.3" with `apple.iphoneMedium`) per locale, MD5-idempotent |
| `apple previews [--poster 5]` | API | app preview videos per locale, checked first, MD5-idempotent, poster frame |
| `apple iap` / `apple iap-review` | API | IAPs (consumable, non-consumable, non-renewing), localizations, price schedules, availability / review screenshots |
| `apple iap-submit [--with-version] [--no-submit]` | API | IAP / subscription / group versions into a review submission (the first of a type together with the app version); asks before adding and before submitting |
| `apple offer-codes [--codes]` | API | offer code campaigns for one-time purchases; custom and one-time-use codes only with `--codes` (asks; codes saved to `.storebound/offer-codes/`) |
| `apple win-back` | API | win-back offers for lapsed subscribers (asks: they go live on their start date) |
| `apple promoted [--enable]` | API | promoted in-app purchases: create (disabled), order, switch on (asks) |
| `apple preorder status \| enable --release-date \| end` | API | pre-orders per territory (enable / end ask first) |
| `apple cpp list \| create \| upload \| submit \| visible` | API | custom product pages from the config and a shots variant; submit / visible ask first |
| `apple events list \| create \| upload \| submit` | API | in-app events with schedule, localizations, card and details images; submit asks first |
| `apple subscriptions [--csv] [--codes]` | API | groups, subscriptions, levels, localizations, availability, equalized prices + overrides + CSV (scheduled changes), introductory and promotional offers, offer code campaigns, review screenshots (plan first, asks; custom codes only with `--codes`) |
| `apple appinfo` | API | categories, age rating, content rights, Free, territories; plan first (old -> new), only changes are sent |
| `apple review-details` | API | App Review contact, demo account (from `.storebound/`, masked), notes (+ sign-in steps, where account deletion is) |
| `apple encryption` | API | export compliance answer from `apple.usesNonExemptEncryption` (never guessed) |
| `apple release --phased on\|pause\|resume\|complete`, `--manual-release` | API | phased release, release an approved version (resume / complete / release ask first) |
| `apple signing status` | API, read-only | bundle id capabilities, distribution certificates (with / without a private key on this Mac), App Store profiles per bundle id, installed or not |
| `apple signing ensure [--keychain] [--no-install]` | API | missing capabilities, a distribution certificate from a key made here (kept 0600 in `.storebound/signing/`, never printed), App Store profiles installed for Xcode; dry first, asks; Developer ID needs the Account Holder (403 explained) |
| `apple build [--version x.y.z] [--build-number N]` | local + API read | xcodebuild archive + exportArchive with a generated ExportOptions.plist (app-store-connect, team, signing style, profiles) and the API key; next build number; Capacitor, React Native, Flutter, Unity projects found |
| `apple upload [] [--via api\|altool]` | API | IPA through the App Store Connect API build upload (or altool), build number checked, waits until VALID (exit 0 / 1 / 7); dry first, asks |
| `apple next-build` | API, read-only | next safe build number |
| `apple status` | API | versions, builds, IAPs, submissions; `--wait-review` (exit 0 / 1 / 7); `--cancel-submission ` |
| `apple ppo status \| create --variant b \| upload --variant b` | API | Product Page Optimization test from `shots` variants: treatments, localizations, screenshot sets |
| `apple ppo start \| stop` | API | start / stop the test (asks first; a stopped test cannot be restarted) |
TestFlight
| command | kind | what |
|---|---|---|
| `apple testflight status` | API, read-only | groups, testers and builds per group, recent builds with internal / external beta states |
| `apple testflight groups` | API | create the groups of `apple.testflight.groups` that are missing |
| `apple testflight testers [--csv f] [--remove] [--invite]` | API | testers from a CSV in `.storebound/` into their groups (asks first; emails masked) |
| `apple testflight what-to-test --build N` | API | "What to Test" per locale from `store/appstore.md` (`whatToTest`, else `whatsNew`) |
| `apple testflight info` | API | Beta App Description, feedback email, URLs, Beta App Review contact and notes |
| `apple testflight distribute --build N --group a,b [--submit-review]` | API | the build to groups; external groups and Beta App Review ask first |
| `apple testflight wait --build N [--timeout 60m]` | API, read-only | until processing ends, with progress; exit 0 VALID / 1 FAILED / 7 timeout |
| `apple testflight expire --keep N \| --older-than D` | API | expire old builds (never attached or processing ones); dry run, then asks |
After launch
| command | kind | what |
|---|---|---|
| `health [--weekly] [--days 7]` | API, read-only | Play vitals (user-perceived crash / ANR vs 1.09 % / 0.47 %, devices, top issues), App Store metrics, diagnostics, TestFlight crashes; `--weekly` writes a Markdown report |
| `reviews list [--unanswered]` | API, read-only | recent reviews of both stores |
| `reviews draft` | API + local | one reply draft per unanswered review in `.storebound/replies/` |
| `reviews send` | API | sends drafts with `approved: true` (checks length and incentives, asks first) |
| `prices export [--no-subscriptions]` | API + local | current prices of every product and subscription (Apple current + scheduled, Play base plan regions), both stores, as `store/prices.csv`; subscription rows feed `apple subscriptions --csv` / `play subscriptions --csv` |
| `prices import [--reconvert]` | API | diff table, then (asks first) Apple price schedules incl. future-dated changes and Play regional prices |
| `analytics setup` | API | App Store analytics report request (once, Admin key) |
| `analytics snapshot [--days 28]` | API + local | impressions, page views, downloads (App Store), visitors, acquisitions, installs (Play reports bucket) -> `.storebound/analytics/` |
| `analytics compare` | local | snapshots vs the git history of the store files: before / after each listing change |
| `reports apple-sales \| apple-finance \| play-earnings \| play-sales` | API, read-only + local | sales / finance reports into `.storebound/reports/`, summary per product and currency, CSV export |
| `triage [--days 14]` | API, read-only | TestFlight crash and screenshot feedback, Play error reports: grouped by signature with devices, OS versions, builds |
| `aso rank [--storefronts us,de]` | network + local | App Store rank per keyword and storefront plus competitors from the public iTunes Search API (rate limited) -> `.storebound/aso/` |
| `aso competitors [--term ] [--storefront us,tr] [--top 10]` | network + local | top apps per keyword (name, developer, rating count, first release, last update, localized genre; no subtitles: not in any public API) and the words in their names you do not cover, same rate limit -> `.storebound/aso/competitors/` |
| `aso suggest [--locale en-US] [--write]` | local (+ Apple Ads API with credentials) | keyword candidates (own text, competitor names, localized category names, Apple Ads keyword suggestions with their relative 0-100 popularity) scored by relevance and competition, and a 100-byte keyword field per locale as a diff; `--write` puts it into `store/appstore.md` as a draft for review. Popularity is never estimated |
| `aso audit` | local | one report: `check --aso`, free keyword room, rank trend arrows, competitor gaps, Play short description and density, App Store conversion from the analytics snapshots, PPO / Play experiment ideas and the next 3 actions |
Screenshots and video
| command | kind | what |
|---|---|---|
| `shots capture [--source web\|ios-sim\|android-emu\|folder] [--concurrency N] [--only-missing \| --force]` | local | raw frames from your running app or game; web: parallel contexts, optional one boot per language (`capture.session`), skips unchanged frames, timing per step |
| `shots fonts` | network | font subsets for your copy (Noto for CJK / Arabic / Cyrillic / Devanagari ...), your own local font files, and the glyph coverage check per language |
| `shots render [--variant b \| --variants] [--concurrency N]` | local | every store size and language in the game or app style, feature graphic, contact sheet, overflow report |
| `shots jpeg` | local | JPEG q92 copies of the Play renders (about 4x faster uploads) |
| `shots check [--variants] [--json]` | local, read-only | store rules: sizes, alpha, format, bytes, counts, overflow, fallback / missing glyphs, videos, folder frames |
| `shots gallery` | local | `/index.html` review page |
| `shots record [--source ...]` | local | app or gameplay clips: web (Chromium screencast), simulator, emulator, folder |
| `shots video [--target iphone,ipad,youtube]` | local | app previews + promo video with captions, poster, checks |
Many apps and release trains
| command | kind | what |
|---|---|---|
| `ws init` | local | find the app folders below and write `storebound.workspace.json` (globs like `games/*`) |
| `ws list [--all]` | local | apps (folders + store-only apps), studios, accounts and whether each credential profile has its keys |
| `ws status [--offline] [--apps a,b]` | API, read-only | portfolio table: every app x both stores (live / rollout % / next version), catalog parity, check; parallel |
| `ws check`, `ws validate [--apps a,b]` | local / API, read-only | the offline check / the readiness report of every app in one table |
| `ws run --apps a,b \| --all [--apply]` | per command | any read command in every app; writes as dry runs unless `--apply`; never submit / release / rollout / send |
| `ws apps import --account ` | API, read-only + local cache | the apps a store account sees, paired with the folders (the desktop's rules) |
| `ws desktop export [--out f \| --merge [--apply]]`, `ws desktop import` | local | the workspace to and from the desktop app's state (labels, never keys) |
| `release status [--app a,b] [--versions]` | API, read-only | both stores on one train per version: build > testing > review > live > rollout % |
| `release plan [--app a]` | API, read-only | what is left for that version on each store, in fix order, with the owner's next step |
## Growth extras
Optional commands for after (or around) the first release. Each has `--dry` and `--json`; store and API writes ask in
a terminal first and are never offered over MCP.
- **Purchasing power prices:** `storebound prices ppp [--base 4.99] [--source worldbank|bigmac|file]` proposes a price
per region from the bundled World Bank ICP PPP conversion factors (`cli/data/ppp-worldbank.json`, CC BY 4.0, dated
and cited; refresh with `bun tools/update-ppp.ts`), clamped to `prices.ppp.floor`..`ceiling` of the US price and
snapped to each store's price points. It writes a sheet in the `prices import` format (default
`store/prices-ppp.csv`) and shows the diff; `storebound prices import --csv store/prices-ppp.csv --dry` applies it.
Skill: `ppp-pricing`.
- **Apple Ads:** `storebound ads apple status | campaigns | report [--by campaign|keyword] [--csv out.csv]`: spend,
taps, installs, CPA, read-only (OAuth client credentials from env / `.storebound/`, never printed). `bid`, `budget`
and `pause` print old -> new and ask first. Skill: `apple-ads`.
- **Web presence:** `storebound web init [--landing]` writes `apple-app-site-association`,
`.well-known/assetlinks.json` (Play app signing SHA-256), the Smart App Banner tag and an optional minimal landing
page (store marks from simple-icons, privacy / support links from the config). `storebound web check `
verifies the live files: 200 without redirects, JSON content type, valid JSON, ids match the config. Skill: `web-presence`.
- **Ad-hoc and internal sharing:** `storebound apple adhoc devices | register | profile | options | export` (UDIDs
masked; registering asks first) and `storebound play share --aab app.aab` (Play internal app sharing; asks first,
prints the install link). Skill: `adhoc-distribution`.
- **RevenueCat (optional):** `storebound revenuecat diff | sync` mirrors products, subscriptions, entitlements and a
default offering into a RevenueCat project (REST API v2, secret key from env / `.storebound/`). sync only creates and
attaches, never deletes. Runtime entitlements, receipts and paywalls stay RevenueCat's job. Skill: `revenuecat-sync`.
- **Console forms without an API, agent-first:** `storebound console plan