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.
898 lines
80 KiB
HTML
898 lines
80 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>storebound · from an empty folder to Submit for review on Google Play and the App Store</title>
|
|
<meta name="description" content="Take a mobile app or game from nothing to Submit for review on Google Play and the App Store: app records, in-app products and subscriptions, listings in every language, every questionnaire, account deletion and review access, privacy, icons, screenshots and previews. One config and your AI coding agent. storebound never presses Submit.">
|
|
<link rel="canonical" href="https://storebound.dev/">
|
|
<meta name="color-scheme" content="light dark">
|
|
<meta name="theme-color" content="#f8fafc" media="(prefers-color-scheme: light)">
|
|
<meta name="theme-color" content="#0b1120" media="(prefers-color-scheme: dark)">
|
|
<link rel="icon" href="favicon.ico?v=2" sizes="16x16 32x32 48x48">
|
|
<link rel="manifest" href="manifest.webmanifest?v=2">
|
|
<link rel="apple-touch-icon" href="apple-touch-icon.png?v=2">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:site_name" content="storebound">
|
|
<meta property="og:url" content="https://storebound.dev/">
|
|
<meta property="og:title" content="storebound · take your app or game from zero to Submit for review">
|
|
<meta property="og:description" content="For mobile apps and games: app records, in-app products and subscriptions, listings in every language, every questionnaire, privacy, icons, screenshots. One config and your AI coding agent. Open source, MIT.">
|
|
<meta property="og:image" content="https://storebound.dev/og.png?v=2">
|
|
<meta property="og:image:width" content="1200">
|
|
<meta property="og:image:height" content="630">
|
|
<meta property="og:image:alt" content="storebound: take a mobile app or game from zero to Submit for review on Google Play and the App Store">
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:title" content="storebound · take your app or game from zero to Submit for review">
|
|
<meta name="twitter:description" content="For mobile apps and games: app records, in-app products and subscriptions, listings in every language, every questionnaire, privacy, icons, screenshots. One config and your AI coding agent.">
|
|
<meta name="twitter:image" content="https://storebound.dev/og.png?v=2">
|
|
<link rel="stylesheet" href="style.css?v=20261013">
|
|
<script>document.documentElement.classList.add('js')</script>
|
|
<script src="main.js?v=20261013" defer></script>
|
|
</head>
|
|
<body>
|
|
<a class="skip" href="#main">Skip to content</a>
|
|
|
|
<header class="top">
|
|
<div class="wrap top-in">
|
|
<a class="brand" href="#top" aria-label="storebound home">
|
|
<img class="mark" src="img/logo-mark.png?v=2" srcset="img/logo-mark.png?v=2 1x, img/logo-mark@2x.png?v=2 2x" width="28" height="28" alt="storebound logo">
|
|
<span>storebound</span>
|
|
</a>
|
|
<nav class="nav-main" aria-label="Primary">
|
|
<a href="#ship">What it does</a>
|
|
<a href="#scan">No rejections</a>
|
|
<a href="#desktop">Desktop app</a>
|
|
<a href="#agents">Your AI</a>
|
|
<a href="#start">Get started</a>
|
|
<a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/README.md" target="_blank" rel="noopener">Docs<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
<a class="nav-git" href="https://git.crt.fyi/CRT/storebound" target="_blank" rel="noopener">git<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="main">
|
|
|
|
<section class="hero" id="top">
|
|
<div class="hero-bg" aria-hidden="true"></div>
|
|
<div class="wrap hero-in">
|
|
<div class="hero-copy">
|
|
<p class="eyebrow"><span class="dot" aria-hidden="true"></span>Open source · CLI · desktop app · for apps and games</p>
|
|
<h1>From an empty folder to <span class="hl">Submit for review</span>.</h1>
|
|
<p class="lede">On Google Play and the App Store. Your AI agent does all of it with storebound: builds, uploads, store text, screenshots in every language and size, in-app products and prices, the compliance forms, even the consoles without an API. <strong>You only press Submit.</strong></p>
|
|
<div class="ctas">
|
|
<a class="btn btn-primary" href="#start">Get started<span class="ic ic-caret-right" aria-hidden="true"></span></a>
|
|
<a class="btn btn-ghost" href="#download">Download the desktop app</a>
|
|
</div>
|
|
<div class="install" role="group" aria-label="Install command">
|
|
<span class="prompt" aria-hidden="true">$</span>
|
|
<code id="install-cmd">bunx storebound init</code>
|
|
<button class="copy" type="button" data-copy="bunx storebound init" aria-label="Copy install command">Copy</button>
|
|
</div>
|
|
<p class="install-note">Needs <a href="https://bun.sh" target="_blank" rel="noopener">Bun<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a> 1.1+. No account, no sign-up, no telemetry.</p>
|
|
</div>
|
|
|
|
<figure class="hero-video" id="teaser" aria-labelledby="teaser-cap">
|
|
<div class="player" data-player>
|
|
<video controls preload="none" playsinline width="1280" height="720" poster="img/teaser-poster.webp?v=3" aria-label="storebound teaser video, with music">
|
|
<source src="img/teaser.mp4?v=3" type="video/mp4">
|
|
</video>
|
|
<button class="play" type="button" hidden aria-label="Play the teaser (with sound)"><span class="ic ic-play" aria-hidden="true"></span><span>Watch the teaser</span></button>
|
|
</div>
|
|
<figcaption id="teaser-cap">The teaser: the demo game from an empty folder to both stores, then the desktop app. Real output and real app frames; has music.</figcaption>
|
|
</figure>
|
|
</div>
|
|
|
|
<div class="wrap">
|
|
<ul class="trust" aria-label="At a glance">
|
|
<li>MIT licensed</li>
|
|
<li>Zero runtime dependencies</li>
|
|
<li>No telemetry</li>
|
|
<li><code>--dry</code> on every write</li>
|
|
<li>25 Agent Skills + MCP server</li>
|
|
</ul>
|
|
<p class="works"><span>Works with</span> Claude Code · Codex · Cursor · Gemini CLI · Copilot · Windsurf · OpenCode · or just the CLI</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="sec" id="ship" aria-labelledby="ship-h">
|
|
<div class="wrap">
|
|
<p class="kicker">1 · The whole job</p>
|
|
<h2 id="ship-h">You ask once. Your agent does the rest.</h2>
|
|
<p class="sub">“Get my app ready for both stores.” storebound plans every step on both stores in fix order, and your agent works through it: API first, its own browser for the consoles without an API, a dry run before every write, a read-back after.</p>
|
|
<div class="ship">
|
|
<figure class="term term-real" aria-label="storebound release plan 1.0.0: the steps left on both stores for a brand-new app, in fix order">
|
|
<div class="term-bar" aria-hidden="true"><span></span><span></span><span></span><em>a brand-new app · both stores</em></div>
|
|
<pre><code><span class="p">$</span> storebound release plan 1.0.0
|
|
<span class="o">[release] Example App: validate (read-only, both stores)</span>
|
|
Example App 1.0.0
|
|
both stores (fix first):
|
|
1. CONFIG App Review needs a demo account (sign-in required) · add reviewDemoAccount to .storebound/credentials.json · review-access skill
|
|
<span class="o"> -> storebound check</span>
|
|
2. CONFIG missing file: store/icon/icon.png
|
|
<span class="o"> -> storebound check</span>
|
|
3. CONFIG missing file: store/icon/foreground.png
|
|
<span class="o"> -> storebound check</span>
|
|
4. CONFIG missing file: store/icon/background.png
|
|
<span class="o"> -> storebound check</span>
|
|
App Store: not on the store
|
|
5. STORE 1.0.0 is not on App Store yet: upload a build (Xcode / Transporter), then create the version with its texts
|
|
<span class="o"> -> storebound apple next-build; storebound apple listing --version 1.0.0 --build <N> --dry</span>
|
|
6. <span class="bl">BLOCKER</span> no App Store version being prepared
|
|
<span class="o"> -> storebound apple listing --version x.y.z</span>
|
|
7. <span class="bl">BLOCKER</span> no build attached to the version
|
|
<span class="c">… 19 more steps on both stores, each with the command that fixes it</span></code></pre>
|
|
<figcaption class="term-cap">Real output, <a href="img/term/release-plan.txt">full text</a></figcaption>
|
|
</figure>
|
|
<ul class="does" aria-label="What your agent does with storebound">
|
|
<li><b>Builds and uploads</b><span>Signing, the next free build number, AAB and IPA to the right track, TestFlight groups.</span></li>
|
|
<li><b>Store text in every language</b><span>Listings for both stores within every limit, translated drafts, What's new from git.</span></li>
|
|
<li><b>Screenshots and previews</b><span>Every store size, every language, from your real app, checked against both stores' specs.</span></li>
|
|
<li><b>In-app products and prices</b><span>Products and subscriptions on both stores from one list, priced in 175 countries.</span></li>
|
|
<li><b>The compliance forms</b><span>Data safety, App Privacy, age rating, export compliance, answered from your config and code. What only you know, it asks once.</span></li>
|
|
<li><b>Even consoles without an API</b><span>A <code>console plan</code> gives the agent the steps, the answers and the buttons it must never press.</span></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h3 class="sub-h">Real output from the demo projects</h3>
|
|
<div class="ship-demo">
|
|
<figure class="demo" aria-labelledby="demo-cap" data-demo>
|
|
<div class="demo-win">
|
|
<div class="demo-bar" aria-hidden="true"><span></span><span></span><span></span><em data-cwd>examples/demo-game</em></div>
|
|
<div class="sc-tabs" role="tablist" aria-label="What storebound does, real output">
|
|
<button type="button" role="tab" id="sc-t1" aria-controls="sc-p1" aria-selected="true">Screenshots</button>
|
|
<button type="button" role="tab" id="sc-t2" aria-controls="sc-p2" aria-selected="false" tabindex="-1">Pre-flight</button>
|
|
<button type="button" role="tab" id="sc-t3" aria-controls="sc-p3" aria-selected="false" tabindex="-1">Store forms</button>
|
|
<button type="button" role="tab" id="sc-t4" aria-controls="sc-p4" aria-selected="false" tabindex="-1">Icons</button>
|
|
</div>
|
|
<div class="sc-stage">
|
|
<!-- Terminal lines, counts and check results are real CLI output from the bundled demos (timestamps and absolute paths trimmed, long lines shortened with …). -->
|
|
<section class="sc sc-shots" id="sc-p1" role="tabpanel" aria-labelledby="sc-t1" data-cwd="examples/demo-game" tabindex="0">
|
|
<div class="sc-head">
|
|
<p class="sc-h">Store screenshots, every size, every language</p>
|
|
<p class="sc-out">One command rendered <b>200 store-ready images</b>: 6 device sizes plus the Play feature graphic, 4 languages, an A/B variant. Then checked them against both stores' specs: <b>0 errors</b>.</p>
|
|
</div>
|
|
<p class="t-cmd" data-s="type"><i>$</i> <span>storebound shots render --variants</span></p>
|
|
<div class="tick" aria-hidden="true">
|
|
<p data-s="22"><b>[shots]</b> store/renders/en/ios69/01.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/en/ios65/02.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/en/ipad13/03.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/tr/play/01.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/tr/tab7/04.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/ja/ios69/02.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/ja/tab10/03.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/ar/ios69/01.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/ar/ipad13/04.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/ar/feature/1024x500.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/b/en/ios69/01.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/b/ja/play/02.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/b/ar/tab10/04.png</p>
|
|
<p data-s="22"><b>[shots]</b> store/renders/b/tr/feature/1024x500.png</p>
|
|
<p class="t-ok" data-s="22"><b>[shots]</b> checked 200 images, 8 videos, 56 sets: 0 errors, 0 warnings</p>
|
|
</div>
|
|
<ul class="shot-grid" aria-label="Rendered App Store screenshots in English, Japanese, Arabic and Turkish">
|
|
<li data-s="45"><img src="img/hero/s-en-1.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>EN</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ja-1.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>JA</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ar-1.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>AR</span></li>
|
|
<li data-s="45"><img src="img/hero/s-tr-1.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>TR</span></li>
|
|
<li data-s="45"><img src="img/hero/s-en-2.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>EN</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ja-2.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>JA</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ar-2.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>AR</span></li>
|
|
<li data-s="45"><img src="img/hero/s-tr-2.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>TR</span></li>
|
|
<li data-s="45"><img src="img/hero/s-en-3.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>EN</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ja-3.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>JA</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ar-3.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>AR</span></li>
|
|
<li data-s="45"><img src="img/hero/s-tr-3.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>TR</span></li>
|
|
<li data-s="45"><img src="img/hero/s-en-4.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>EN</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ja-4.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>JA</span></li>
|
|
<li data-s="45"><img src="img/hero/s-ar-4.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>AR</span></li>
|
|
<li data-s="45"><img src="img/hero/s-tr-4.webp" width="120" height="261" alt="" loading="lazy" decoding="async"><span>TR</span></li>
|
|
</ul>
|
|
<p class="sc-foot" data-s="300"><b class="big"><span data-count="200">200</span> images</b> <span class="sep">·</span> iPhone 6.9" and 6.5", iPad 13", Play phone, 7" and 10" tablet, feature graphic</p>
|
|
<p class="sc-hand"><span>By hand</span> 200 exports from a design tool, each dropped into the right slot in two consoles.</p>
|
|
</section>
|
|
|
|
<section class="sc sc-pf" id="sc-p2" role="tabpanel" aria-labelledby="sc-t2" data-cwd="examples/demo-app" tabindex="0" hidden>
|
|
<div class="sc-head">
|
|
<p class="sc-h">Catches what gets you rejected, before you upload</p>
|
|
<p class="sc-out">Checked a sign-in app against 7 store requirements, offline. <b>3 would have stopped it</b>, each named with its fix. Fixed, re-checked: <b>all 7 pass</b>.</p>
|
|
</div>
|
|
<div class="pf-strip">
|
|
<div class="tick tick-pf" aria-hidden="true">
|
|
<p class="t-cmd" data-s="type" data-o="1"><i>$</i> <span>storebound check</span></p>
|
|
<p class="t-err" data-s="2000" data-o="10" data-pill="bad">error: 3 problems</p>
|
|
<p class="t-cmd" data-s="type" data-o="11"><i>$</i> <span>storebound init</span></p>
|
|
<p class="t-out" data-s="200" data-o="12"><b>[init]</b> create store/listing.md</p>
|
|
<p class="t-out" data-s="500" data-o="13"><b>[init]</b> create store/appstore.md</p>
|
|
<p class="t-note" data-s="1100" data-o="14"># you add the App Review demo account to .storebound/ (never chat, never git)</p>
|
|
<p class="t-cmd" data-s="type" data-o="15"><i>$</i> <span>storebound check</span></p>
|
|
<p class="t-ok" data-s="260" data-o="16" data-flip="pf-1"><b>[check]</b> play listing: 2 languages</p>
|
|
<p class="t-ok" data-s="260" data-o="17" data-flip="pf-2"><b>[check]</b> appstore listing: 2 locales</p>
|
|
<p class="t-ok t-strong" data-s="200" data-o="18" data-flip="pf-6" data-pill="good"><b>[check]</b> all good</p>
|
|
</div>
|
|
<p class="sc-pill" aria-hidden="true"><span class="pill-bad">3 to fix</span><span class="pill-good">7 of 7 pass</span></p>
|
|
</div>
|
|
<ol class="pf-list" aria-label="What storebound check verified">
|
|
<li class="pf-row" id="pf-1" data-bad data-s="260" data-o="2"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">Play listing<small>title 30 · short 80 · full 4000</small></span><span class="pf-det"><span class="d-bad">store/listing.md is missing · fix: <code>storebound init</code></span><span class="d-ok">2 languages, every field within limits <em>fixed by init</em></span></span></li>
|
|
<li class="pf-row" id="pf-2" data-bad data-s="260" data-o="3"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">App Store listing<small>name · subtitle · keywords 100</small></span><span class="pf-det"><span class="d-bad">store/appstore.md is missing · fix: <code>storebound init</code></span><span class="d-ok">2 locales, every field within limits <em>fixed by init</em></span></span></li>
|
|
<li class="pf-row" data-s="140" data-o="4"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">Screenshot captions<small>every scene, every language</small></span><span class="pf-det"><span class="d-ok">2 languages × 4 scenes, none missing</span></span></li>
|
|
<li class="pf-row" data-s="140" data-o="5"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">Play Data safety<small>the form Play requires</small></span><span class="pf-det"><span class="d-ok">764 answers built from the config</span></span></li>
|
|
<li class="pf-row" data-s="140" data-o="6"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">Account deletion<small>Play web page · Apple 5.1.1(v)</small></span><span class="pf-det"><span class="d-ok">web page and in-app path both set</span></span></li>
|
|
<li class="pf-row" id="pf-6" data-bad data-s="260" data-o="7"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">App Review sign-in<small>Apple 2.1 · Play App access</small></span><span class="pf-det"><span class="d-bad">sign-in required, no demo account · fix: add it to <code>.storebound/</code></span><span class="d-ok">demo account set, kept only in .storebound/ <em>fixed by you</em></span></span></li>
|
|
<li class="pf-row" data-s="140" data-o="8"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">Secrets stay out of git<small>store keys, demo account</small></span><span class="pf-det"><span class="d-ok">.storebound/ is in .gitignore</span></span></li>
|
|
<li class="pf-row pf-na" data-s="140" data-o="9"><span class="pf-ic" aria-hidden="true"></span><span class="pf-what">In-app products, subscriptions<small>price texts, terms URL</small></span><span class="pf-det"><span class="d-ok">none in this app, skipped</span></span></li>
|
|
</ol>
|
|
<p class="sc-hand"><span>Without it</span> a missing demo account comes back as a Guideline 2.1 rejection.</p>
|
|
</section>
|
|
|
|
<section class="sc sc-forms" id="sc-p3" role="tabpanel" aria-labelledby="sc-t3" data-cwd="examples/demo-app" tabindex="0" hidden>
|
|
<div class="sc-head">
|
|
<p class="sc-h">Store forms answered from your config</p>
|
|
<p class="sc-out">Play's Data safety form: <b>764 answers</b> derived from 12 lines of config, as the CSV Play imports. What only you can know, like encryption, <b>it asks you</b>. It never guesses.</p>
|
|
</div>
|
|
<p class="t-cmd" data-s="type"><i>$</i> <span>storebound play data-safety --dry</span></p>
|
|
<p class="t-out t-wrap" data-s="200"><b>[play]</b> data safety: 4 data types declared (PSL_EMAIL, PSL_USER_ACCOUNT, PSL_USER_GENERATED_CONTENT, PSL_CRASH_LOGS)</p>
|
|
<!-- The CSV streams in file order, then the final frame keeps only its key rows (data-key): the same real rows, in file order (the data types are in the [play] line above). -->
|
|
<div class="tick tick-csv" id="csv" aria-hidden="true">
|
|
<p data-s="24" data-key><em>TRUE</em> <span class="lbl">Does your app collect or share any of the required user data types?</span></p>
|
|
<p data-s="24" data-key class="hot"><em>TRUE</em> <span class="lbl">Is all of the user data collected by your app encrypted in transit?</span></p>
|
|
<p data-s="24" data-key class="hot"><em>TRUE</em> <span class="lbl">Do you provide a way for users to request that their data is deleted?</span></p>
|
|
<p data-s="24" data-key><em>TRUE</em> <span class="lbl">Account creation / PSL_ACM_USER_ID_PASSWORD</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">PSL_DATA_TYPES_PERSONAL / PSL_EMAIL</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">PSL_DATA_TYPES_PERSONAL / PSL_USER_ACCOUNT</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">PSL_DATA_TYPES_CALENDAR / PSL_CALENDAR</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">PSL_DATA_TYPES_APP_PERFORMANCE / PSL_CRASH_LOGS</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">PSL_DATA_TYPES_APP_ACTIVITY / PSL_USER_GENERATED_CONTENT</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_EMAIL) / Collected</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_EMAIL) / Shared</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_EMAIL) / Required</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_EMAIL) / Collection purpose</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_EMAIL) / Collection purpose</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_USER_ACCOUNT) / Collected</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_USER_ACCOUNT) / Required</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_USER_ACCOUNT) / Collection purpose</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_USER_ACCOUNT) / Collection purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_USER_ACCOUNT) / Sharing purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_PHONE) / Collection purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_RACE_ETHNICITY) / Sharing purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_SEXUAL_ORIENTATION_GENDER_IDENTITY) / Collection purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_PURCHASE_HISTORY) / Required</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_CREDIT_SCORE) / Sharing purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_OTHER) / Collection purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_PRECISE_LOCATION) / Processed ephemerally</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_WEB_BROWSING_HISTORY) / Sharing purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_SMS_CALL_LOG) / Collection purpose</span></p>
|
|
<p data-s="24"><i>-</i> <span class="lbl">Data usage and handling (PSL_PHOTOS) / Collected</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_CRASH_LOGS) / Collected</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_CRASH_LOGS) / Required</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_CRASH_LOGS) / Collection purpose</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_USER_GENERATED_CONTENT) / Collected</span></p>
|
|
<p data-s="24"><em>TRUE</em> <span class="lbl">Data usage and handling (PSL_USER_GENERATED_CONTENT) / Required</span></p>
|
|
</div>
|
|
<p class="sc-foot" data-s="500" data-flip="csv"><b class="big"><span data-count="764">764</span> answers</b> <span class="sep">·</span> from 12 lines of config</p>
|
|
<p class="t-cmd" data-s="type"><i>$</i> <span>storebound apple encryption --dry</span></p>
|
|
<p class="t-err t-wrap" data-s="300">error: apple.usesNonExemptEncryption is not set in storebound.config.ts. Decide it with the export-compliance skill (…); storebound never guesses it.</p>
|
|
<p class="sc-hand"><span>By hand</span> Play's form, question by question, for every data type you collect.</p>
|
|
</section>
|
|
|
|
<section class="sc sc-icons" id="sc-p4" role="tabpanel" aria-labelledby="sc-t4" data-cwd="examples/demo-game" tabindex="0" hidden>
|
|
<div class="sc-head">
|
|
<p class="sc-h">Every icon both stores and the web need</p>
|
|
<p class="sc-out">One 1024 px artwork became <b>21 icons</b>: App Store, Google Play, the Android launcher in 5 densities (round and legacy), Capacitor layers, web and favicons.</p>
|
|
</div>
|
|
<p class="t-cmd" data-s="type"><i>$</i> <span>storebound icons</span></p>
|
|
<div class="ic-row">
|
|
<figure class="ic-src" data-s="300"><img src="img/hero/i-src.webp" width="76" height="76" alt="The one artwork: a drop over a rainbow puddle" decoding="async"><figcaption>icon.png</figcaption></figure>
|
|
<span class="ic-arrow" aria-hidden="true" data-s="150">→</span>
|
|
<ul class="ic-out" aria-label="Icons storebound wrote">
|
|
<li data-s="70"><img src="img/hero/i-1024.webp" width="60" height="60" class="ic-sq" alt="" loading="lazy" decoding="async"><small>1024 iOS</small></li>
|
|
<li data-s="70"><img src="img/hero/i-play.webp" width="52" height="52" class="ic-sq" alt="" loading="lazy" decoding="async"><small>512 Play</small></li>
|
|
<li data-s="70"><img src="img/hero/i-mask.webp" width="44" height="44" class="ic-ro" alt="" loading="lazy" decoding="async"><small>maskable</small></li>
|
|
<li data-s="70"><img src="img/hero/i-192.webp" width="40" height="40" class="ic-sq" alt="" loading="lazy" decoding="async"><small>192 web</small></li>
|
|
<li data-s="70"><img src="img/hero/i-round.webp" width="40" height="40" alt="" loading="lazy" decoding="async"><small>round</small></li>
|
|
<li data-s="70"><img src="img/hero/i-legacy.webp" width="40" height="40" alt="" loading="lazy" decoding="async"><small>legacy</small></li>
|
|
<li data-s="70"><img src="img/hero/i-64.webp" width="28" height="28" alt="" loading="lazy" decoding="async"><small>64 fav</small></li>
|
|
<li data-s="70"><img src="img/hero/i-32.webp" width="16" height="16" alt="" loading="lazy" decoding="async"><small>32 fav</small></li>
|
|
</ul>
|
|
</div>
|
|
<div class="tick tick-ic" aria-hidden="true">
|
|
<p data-s="20"><b>[icons]</b> store/icons/app-store-1024.png <span class="kb">105.2 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/play-512.png <span class="kb">40.7 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/web/icon-512.png <span class="kb">17.5 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/web/icon-192.png <span class="kb">5.6 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/web/icon-180.png <span class="kb">5.2 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/web/favicon-64.png <span class="kb">2.2 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/web/favicon-32.png <span class="kb">1.6 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/web/maskable-512.webp <span class="kb">7.2 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/capacitor/icon-only.png <span class="kb">105.2 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/capacitor/icon-background.png <span class="kb">16.5 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/capacitor/icon-foreground.png <span class="kb">104.8 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-mdpi/ic_launcher_round.png <span class="kb">1.9 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-mdpi/ic_launcher.png <span class="kb">1.6 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-hdpi/ic_launcher_round.png <span class="kb">3.0 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-hdpi/ic_launcher.png <span class="kb">2.6 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-xhdpi/ic_launcher_round.png <span class="kb">4.1 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-xhdpi/ic_launcher.png <span class="kb">3.6 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-xxhdpi/ic_launcher_round.png <span class="kb">7.1 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-xxhdpi/ic_launcher.png <span class="kb">6.1 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-xxxhdpi/ic_launcher_round.png <span class="kb">10.0 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> store/icons/android/res/mipmap-xxxhdpi/ic_launcher.png <span class="kb">8.8 KB</span></p>
|
|
<p data-s="20"><b>[icons]</b> icons done (4s)</p>
|
|
</div>
|
|
<p class="sc-foot" data-s="300"><b class="big"><span data-count="21">21</span> icons from one artwork</b> <span class="sep">·</span> in 4 s</p>
|
|
<p class="sc-hand"><span>By hand</span> 21 exports at exact sizes, each into the right folder.</p>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<figcaption id="demo-cap" class="demo-cap">Real output · <code>examples/demo-game</code> and <code>demo-app</code> · offline commands and dry runs, nothing sent to a store</figcaption>
|
|
</figure>
|
|
</div>
|
|
|
|
<h3 class="sub-h" id="art">Store art, every size, every language</h3>
|
|
<figure class="shot shot-wide">
|
|
<img src="img/showcase.webp" width="1600" height="505" alt="Store screenshots rendered by storebound: four scenes of the demo game in English plus the hero scene in Turkish, Japanese and Arabic" loading="lazy" decoding="async">
|
|
<figcaption>The demo game “Prism Puddle”: four scenes in English, the hero scene in Turkish, Japanese and Arabic (right to left). Rendered by <code>shots render</code>, checked by <code>shots check</code>.</figcaption>
|
|
</figure>
|
|
|
|
<h3 class="sub-h">The consoles, too</h3>
|
|
<div class="ship">
|
|
<figure class="term term-real" aria-label="storebound console plan play-content-rating: start URL, steps, the buttons never to press and the owner's final button">
|
|
<div class="term-bar" aria-hidden="true"><span></span><span></span><span></span><em>the agent drives the console</em></div>
|
|
<pre><code><span class="p">$</span> storebound console plan play-content-rating
|
|
Play Console: Content rating (IARC)
|
|
start: https://play.google.com/console
|
|
then: Pick the developer account if asked (set play.developerId for a deep link)
|
|
then: the app (com.example.prismpuddle) in the app list
|
|
then: Policy and programs > App content
|
|
then: Content rating > Start (or Manage)
|
|
Steps:
|
|
1. Open App content > Content ratings (/app-content/content-rating-overview), then "Start questionnaire": it opens /app-content/content-rating-iarc-questionnaire. [click: "Start questionnaire"]
|
|
2. Page 1: type the email address and pick the category (material radio: JS click on the inner [role=radio]). The IARC Terms of Use checkbox is an agreement: never tick it; the owner ticks it (it is in the batch). Then Next. [click: "Next"]
|
|
9. Summary: stop. Do NOT press its Save (the final button here is "Save", not "Submit"): tell the owner the ratings shown and let them press Save (it applies the IARC ratings).
|
|
Never press: "IARC Terms of Use box", "Save (Summary page)", "Publish", "Submit", "Create", "Create app", "Send", "Release", "Definitions and Examples", "?", "More information"
|
|
Final button (the owner's): Save (Summary page)
|
|
<span class="c">… the answer sheet, the in-page helper and how to verify</span></code></pre>
|
|
<figcaption class="term-cap">Real output, <a href="img/term/console-plan.txt">full text</a></figcaption>
|
|
</figure>
|
|
<div class="cols-you">
|
|
<p class="sub">Some store pages have no API: creating the app records, Play's App content cards and content rating, App Privacy labels. <code>storebound console plan</code> hands your agent the start URL, every step, the answer sheet from your config and the buttons it must never press. The agent drives the page and stops before the final button.</p>
|
|
<div class="you-box">
|
|
<span class="tag t-you">YOU</span>
|
|
<ul>
|
|
<li><strong>App Store: Submit for Review · Play: Send changes for review</strong></li>
|
|
<li>Releasing an approved version, completing a rollout</li>
|
|
<li>App Privacy Publish, agreements, banking and tax</li>
|
|
<li>The answers only you can know, asked once, in one batch</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="note">Every step, what is API, what the agent drives and what stays yours: <a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/journey.md" target="_blank" rel="noopener">the journey<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="sec alt" id="scan" aria-labelledby="scan-h">
|
|
<div class="wrap">
|
|
<p class="kicker">2 · No avoidable rejections</p>
|
|
<h2 id="scan-h">You won't get rejected for the avoidable things.</h2>
|
|
<p class="sub"><code>storebound scan</code> reads your project offline and read-only: Xcode and Gradle files, Info.plist, manifests, entitlements, lockfiles and engine settings, plus your config and listings. It works out the stack and the SDKs, then reports every rejection risk it can prove, each with the guideline, the evidence as <code>file:line</code>, the fix, a severity and the skill to open.</p>
|
|
<figure class="term term-real" aria-label="storebound scan: rejection risks with the guideline, the evidence as file:line and the fix">
|
|
<div class="term-bar" aria-hidden="true"><span></span><span></span><span></span><em>tests/fixtures/scan/teaser</em></div>
|
|
<pre><code><span class="p">$</span> storebound scan
|
|
Rejection risks: 2 blocker, 3 high, 0 medium, 0 low
|
|
1. <span class="bl">BLOCKER</span> 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
|
|
<span class="o"> fix: Create a demo account that sees every feature (not a sign-up the reviewer must do), put it in .storebound/credentials.json as reviewDemoAccount (never in the repo), then `storebound apple review-details`.</span>
|
|
<span class="o"> skill: review-access · https://developer.apple.com/app-store/review/guidelines/</span>
|
|
2. <span class="bl">BLOCKER</span> 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 {
|
|
<span class="o"> fix: Add "Delete account" in the app (it deletes the account and its data, not only signs out; with Sign in with Apple also revoke the token), then set accounts.inApp to where it starts, e.g. "Settings > Account > Delete account".</span>
|
|
<span class="c">… 3 more findings (4.8, 3.1.1, 5.1.2(i)), then the Data safety / App Privacy proposal</span></code></pre>
|
|
<figcaption class="term-cap">Real output, <a href="img/term/scan.txt">full text</a></figcaption>
|
|
</figure>
|
|
<h3 class="rules-h">What it proves, on both stores</h3>
|
|
<ul class="rules" aria-label="Scan rules, summarized">
|
|
<li><span class="rg"><b>2.1(a)</b><i>App access</i></span><p>A sign-in with no demo account for the reviewer.</p></li>
|
|
<li><span class="rg"><b>5.1.1(v)</b><i>Account deletion</i></span><p>Sign-up or social login without an in-app deletion path or a deletion URL.</p></li>
|
|
<li><span class="rg"><b>4.8</b></span><p>Google or Facebook login without Sign in with Apple or another equivalent option.</p></li>
|
|
<li><span class="rg"><b>3.1.1, 3.1.2</b><i>Payments</i></span><p>No Restore Purchases path, card SDKs next to in-app products, missing subscription terms.</p></li>
|
|
<li><span class="rg"><b>5.1.2</b><i>Data safety</i></span><p>Tracking SDKs without ATT; SDK data types missing from App Privacy or Data safety.</p></li>
|
|
<li><span class="rg"><b>5.1.1, ITMS-91053</b></span><p>Permissions without purpose strings; a missing or incomplete privacy manifest.</p></li>
|
|
<li><span class="rg"><i>Restricted permissions</i></span><p>Background location, photos, all files, accessibility, exact alarms, foreground services, and the Console declaration each one needs.</p></li>
|
|
<li><span class="rg"><b>1.3</b><i>Families</i></span><p>Ad or analytics SDKs in a Kids Category or child-directed app.</p></li>
|
|
<li><span class="rg"><i>Target API, 16 KB pages</i></span><p>targetSdk below 36; Unity, React Native, NDK or AGP versions without 16 KB page support.</p></li>
|
|
<li><span class="rg"><b>2.3.7, 2.3.10</b><i>Metadata</i></span><p>Other apps' names, "best / #1", keyword lists, other stores in App Store text, placeholder text.</p></li>
|
|
<li><span class="rg"><b>2.2</b></span><p>"Beta", "demo" or "test" wording in the app name and the listing.</p></li>
|
|
<li><span class="rg"><b>4.3</b><i>Repetitive content</i></span><p><code>scan --originality --against <folders></code> adds the near-duplicate check against your other apps.</p></li>
|
|
</ul>
|
|
<ul class="legend" aria-label="Legend">
|
|
<li><span class="rg"><b>4.8</b></span> App Store guideline</li>
|
|
<li><span class="rg"><i>Families</i></span> Google Play policy</li>
|
|
</ul>
|
|
<p class="note">Below the risks comes a proposal for Data safety, App Privacy, ATT and the privacy manifest from the detected SDKs, each answer marked detected or ask owner. Nothing is written unless you run <code>scan --write</code>. <code>validate</code> includes the open findings, and the desktop app shows them on its Checks tab.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="sec" id="desktop" aria-labelledby="desktop-h">
|
|
<div class="wrap">
|
|
<p class="kicker">3 · The desktop app</p>
|
|
<h2 id="desktop-h">Every app on both stores, in one window.</h2>
|
|
<p class="sub">Releases, charts, catalog and prices side by side, crash health, reviews, TestFlight and the rejection checks, for every app and game you ship. Send for review from there too: a dry run first, then a confirm. Same storebound underneath; store keys stay in the system keychain.</p>
|
|
<div class="tabs desk-tabs" data-tabs>
|
|
<div class="tablist" role="tablist" aria-label="Desktop app screens">
|
|
<button role="tab" id="dt-send-for-review" aria-controls="dp-send-for-review" aria-selected="true">Send for review</button>
|
|
<button role="tab" id="dt-portfolio" aria-controls="dp-portfolio" aria-selected="false" tabindex="-1">Portfolio</button>
|
|
<button role="tab" id="dt-growth" aria-controls="dp-growth" aria-selected="false" tabindex="-1">Growth</button>
|
|
<button role="tab" id="dt-prices" aria-controls="dp-prices" aria-selected="false" tabindex="-1">Prices</button>
|
|
<button role="tab" id="dt-catalog" aria-controls="dp-catalog" aria-selected="false" tabindex="-1">Catalog</button>
|
|
<button role="tab" id="dt-checks" aria-controls="dp-checks" aria-selected="false" tabindex="-1">Checks</button>
|
|
<button role="tab" id="dt-testflight" aria-controls="dp-testflight" aria-selected="false" tabindex="-1">TestFlight</button>
|
|
<button role="tab" id="dt-console-plan" aria-controls="dp-console-plan" aria-selected="false" tabindex="-1">Console plan</button>
|
|
</div>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-send-for-review" aria-labelledby="dt-send-for-review" tabindex="0">
|
|
<picture>
|
|
<source srcset="img/desktop/send-for-review-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/send-for-review-light.webp" width="1280" height="800" alt="storebound desktop, Releases › Send for review: the dry run of the submission with the build picked by number, then a confirm. You press the button." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>Releases › Send for review: the dry run of the submission with the build picked by number, then a confirm. You press the button.</figcaption>
|
|
</figure>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-portfolio" aria-labelledby="dt-portfolio" tabindex="0" hidden>
|
|
<picture>
|
|
<source srcset="img/desktop/portfolio-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/portfolio-light.webp" width="1280" height="800" alt="storebound desktop, Portfolio: every app of every studio, App Store and Google Play side by side." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>Portfolio: every app of every studio, App Store and Google Play side by side.</figcaption>
|
|
</figure>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-growth" aria-labelledby="dt-growth" tabindex="0" hidden>
|
|
<picture>
|
|
<source srcset="img/desktop/growth-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/growth-light.webp" width="1280" height="800" alt="storebound desktop, Growth: store analytics charted, the day under the pointer." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>Growth: store analytics charted, the day under the pointer.</figcaption>
|
|
</figure>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-prices" aria-labelledby="dt-prices" tabindex="0" hidden>
|
|
<picture>
|
|
<source srcset="img/desktop/prices-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/prices-light.webp" width="1280" height="800" alt="storebound desktop, Prices: products by key territory, both stores side by side, the gaps marked." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>Prices: products by key territory, both stores side by side, the gaps marked.</figcaption>
|
|
</figure>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-catalog" aria-labelledby="dt-catalog" tabindex="0" hidden>
|
|
<picture>
|
|
<source srcset="img/desktop/catalog-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/catalog-light.webp" width="1280" height="800" alt="storebound desktop, Catalog: every product and subscription on both stores against the config." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>Catalog: every product and subscription on both stores against the config.</figcaption>
|
|
</figure>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-checks" aria-labelledby="dt-checks" tabindex="0" hidden>
|
|
<picture>
|
|
<source srcset="img/desktop/checks-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/checks-light.webp" width="1280" height="800" alt="storebound desktop, Checks: the rejection risks with evidence and the fix, and store readiness." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>Checks: the rejection risks with evidence and the fix, and store readiness.</figcaption>
|
|
</figure>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-testflight" aria-labelledby="dt-testflight" tabindex="0" hidden>
|
|
<picture>
|
|
<source srcset="img/desktop/testflight-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/testflight-light.webp" width="1280" height="800" alt="storebound desktop, TestFlight: groups, testers and builds." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>TestFlight: groups, testers and builds.</figcaption>
|
|
</figure>
|
|
<figure class="panel desk-panel" role="tabpanel" id="dp-console-plan" aria-labelledby="dt-console-plan" tabindex="0" hidden>
|
|
<picture>
|
|
<source srcset="img/desktop/console-plan-dark.webp" media="(prefers-color-scheme: dark)">
|
|
<img src="img/desktop/console-plan-light.webp" width="1280" height="800" alt="storebound desktop, Console plan: steps, answers and never-press buttons for a console form without an API." loading="lazy" decoding="async">
|
|
</picture>
|
|
<figcaption>Console plan: steps, answers and never-press buttons for a console form without an API.</figcaption>
|
|
</figure>
|
|
</div>
|
|
|
|
<div class="dl" id="download">
|
|
<div class="dl-head">
|
|
<h3>Download storebound desktop</h3>
|
|
<p class="dl-status" id="dl-status" aria-live="polite">macOS, Windows and Linux. Looking up the latest release…</p>
|
|
</div>
|
|
<ul class="dl-os" aria-label="Downloads">
|
|
<li><a class="btn btn-ghost is-off" data-dl="macos-universal-dmg" aria-disabled="true"><b>macOS</b><small>11+, Apple silicon and Intel · .dmg</small></a></li>
|
|
<li><a class="btn btn-ghost is-off" data-dl="windows-x86_64-nsis" aria-disabled="true"><b>Windows</b><small>10 / 11 · x64 installer</small></a></li>
|
|
<li><a class="btn btn-ghost is-off" data-dl="linux-x86_64-appimage" aria-disabled="true"><b>Linux</b><small>x64 AppImage</small></a></li>
|
|
<li><a class="btn btn-ghost is-off" data-dl="linux-x86_64-deb" aria-disabled="true"><b>Debian / Ubuntu</b><small>x64 .deb</small></a></li>
|
|
</ul>
|
|
<div id="dl-notice"></div>
|
|
<noscript><p class="note">The download list needs JavaScript. Without it: <a href="desktop/latest.json">desktop/latest.json</a> lists every file with its SHA-256.</p></noscript>
|
|
<details class="dl-verify" id="verify">
|
|
<summary>Checksums and signatures</summary>
|
|
<table class="dl-table" id="dl-table" hidden>
|
|
<thead><tr><th scope="col">System</th><th scope="col">File</th><th scope="col">SHA-256</th></tr></thead>
|
|
<tbody id="dl-rows"></tbody>
|
|
</table>
|
|
<ol class="dl-steps">
|
|
<li>Checksum: <code>shasum -a 256 <file></code> (macOS, Linux) or <code>Get-FileHash <file></code> (Windows) must print the SHA-256 above.</li>
|
|
<li>Signature: every file has a <code>.sig</code> next to it (the app's update key, minisign). With <a href="https://jedisct1.github.io/minisign/" target="_blank" rel="noopener">minisign<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>: <code>base64 -d <file>.sig > f.minisig && minisign -Vm <file> -x f.minisig -P <key></code>, with the public key
|
|
<code class="dl-key">RWQqOO/ZeIr3RBmolt/fx108m89QNEUndRTFrucjx0CBMoeFb7/xhrvW</code></li>
|
|
<li>Or let the CLI do both: <code>storebound desktop --download</code> saves the installer to ~/Downloads after checking its signature (it never installs anything).</li>
|
|
</ol>
|
|
<p class="fine">The app updates itself from this site: each update is signed, and the app checks the signature before it replaces anything. It asks before installing. The npm package <code>storebound</code> stays the CLI, MCP server and agent skills; the app brings its own copy of the CLI.</p>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="sec alt" id="agents" aria-labelledby="agents-h">
|
|
<div class="wrap">
|
|
<p class="kicker">4 · Works with your AI</p>
|
|
<h2 id="agents-h">Your AI coding agent drives it. Safely.</h2>
|
|
<p class="sub">Skills, an MCP server and the CLI: the same tools for every agent. The skills are plain Markdown and the CLI is a normal command, so nothing requires a particular tool, or any agent at all.</p>
|
|
<ul class="badges" aria-label="Supported AI tools">
|
|
<li>Claude Code</li>
|
|
<li>OpenAI Codex</li>
|
|
<li>Cursor</li>
|
|
<li>GitHub Copilot</li>
|
|
<li>Gemini CLI</li>
|
|
<li>Windsurf</li>
|
|
<li>OpenCode</li>
|
|
<li>anything that reads AGENTS.md</li>
|
|
</ul>
|
|
<div class="session">
|
|
<div class="agent-setup">
|
|
<h3 class="sub-h">Connect your tool</h3>
|
|
<div class="tabs" data-tabs>
|
|
<div class="tablist" role="tablist" aria-label="AI tool">
|
|
<button role="tab" id="tab-claude" aria-controls="p-claude" aria-selected="true">Claude Code</button>
|
|
<button role="tab" id="tab-codex" aria-controls="p-codex" aria-selected="false" tabindex="-1">Codex</button>
|
|
<button role="tab" id="tab-gemini" aria-controls="p-gemini" aria-selected="false" tabindex="-1">Gemini CLI</button>
|
|
<button role="tab" id="tab-cursor" aria-controls="p-cursor" aria-selected="false" tabindex="-1">Cursor</button>
|
|
<button role="tab" id="tab-copilot" aria-controls="p-copilot" aria-selected="false" tabindex="-1">Copilot</button>
|
|
<button role="tab" id="tab-windsurf" aria-controls="p-windsurf" aria-selected="false" tabindex="-1">Windsurf</button>
|
|
<button role="tab" id="tab-opencode" aria-controls="p-opencode" aria-selected="false" tabindex="-1">OpenCode</button>
|
|
<button role="tab" id="tab-agents" aria-controls="p-agents" aria-selected="false" tabindex="-1">AGENTS.md</button>
|
|
</div>
|
|
|
|
<div class="panel" role="tabpanel" id="p-claude" aria-labelledby="tab-claude" tabindex="0">
|
|
<h4 class="panel-h">Claude Code</h4>
|
|
<div class="code"><button class="copy" type="button" data-copy-target="c-claude" aria-label="Copy Claude Code commands">Copy</button>
|
|
<pre id="c-claude"><code>/plugin marketplace add https://git.crt.fyi/CRT/storebound.git
|
|
/plugin install storebound@crtkafa</code></pre></div>
|
|
<p>Skills, MCP server and <code>storebound</code> on PATH. Or per project: <code>bunx storebound install <span class="nw">--agent claude</span></code></p>
|
|
</div>
|
|
<div class="panel" role="tabpanel" id="p-codex" aria-labelledby="tab-codex" tabindex="0">
|
|
<h4 class="panel-h">OpenAI Codex</h4>
|
|
<div class="code"><button class="copy" type="button" data-copy-target="c-codex" aria-label="Copy Codex command">Copy</button>
|
|
<pre id="c-codex"><code>bunx storebound install --agent codex</code></pre></div>
|
|
<p>Skills in <code>.agents/skills</code>, MCP in <code>.codex/config.toml</code> (Codex reads it for trusted projects). The repo is also a Codex plugin, skills only.</p>
|
|
</div>
|
|
<div class="panel" role="tabpanel" id="p-gemini" aria-labelledby="tab-gemini" tabindex="0">
|
|
<h4 class="panel-h">Gemini CLI</h4>
|
|
<div class="code"><button class="copy" type="button" data-copy-target="c-gemini" aria-label="Copy Gemini CLI command">Copy</button>
|
|
<pre id="c-gemini"><code>gemini extensions install https://git.crt.fyi/CRT/storebound.git</code></pre></div>
|
|
<p>Skills, MCP server and <code>GEMINI.md</code>. Or <code>bunx storebound install --agent gemini</code></p>
|
|
</div>
|
|
<div class="panel" role="tabpanel" id="p-cursor" aria-labelledby="tab-cursor" tabindex="0">
|
|
<h4 class="panel-h">Cursor</h4>
|
|
<div class="code"><button class="copy" type="button" data-copy-target="c-cursor" aria-label="Copy Cursor command">Copy</button>
|
|
<pre id="c-cursor"><code>bunx storebound install --agent cursor</code></pre></div>
|
|
<p>Skills in <code>.agents/skills</code> and <code>.cursor/mcp.json</code>. The repo's <code>.cursor/rules/storebound.mdc</code> is an agent-requested rule you can copy.</p>
|
|
</div>
|
|
<div class="panel" role="tabpanel" id="p-copilot" aria-labelledby="tab-copilot" tabindex="0">
|
|
<h4 class="panel-h">VS Code / GitHub Copilot</h4>
|
|
<div class="code"><button class="copy" type="button" data-copy-target="c-copilot" aria-label="Copy Copilot command">Copy</button>
|
|
<pre id="c-copilot"><code>bunx storebound install --agent copilot</code></pre></div>
|
|
<p>Skills in <code>.github/skills</code> and <code>.vscode/mcp.json</code>.</p>
|
|
</div>
|
|
<div class="panel" role="tabpanel" id="p-windsurf" aria-labelledby="tab-windsurf" tabindex="0">
|
|
<h4 class="panel-h">Windsurf (Devin Desktop)</h4>
|
|
<div class="code"><button class="copy" type="button" data-copy-target="c-windsurf" aria-label="Copy Windsurf command">Copy</button>
|
|
<pre id="c-windsurf"><code>bunx storebound install --agent windsurf</code></pre></div>
|
|
<p>Skills in <code>.agents/skills</code>, MCP in your global <code>mcp_config.json</code>.</p>
|
|
</div>
|
|
<div class="panel" role="tabpanel" id="p-opencode" aria-labelledby="tab-opencode" tabindex="0">
|
|
<h4 class="panel-h">OpenCode</h4>
|
|
<div class="code"><button class="copy" type="button" data-copy-target="c-opencode" aria-label="Copy OpenCode command">Copy</button>
|
|
<pre id="c-opencode"><code>bunx storebound install --agent opencode</code></pre></div>
|
|
<p>Skills in <code>.opencode/skills</code> and <code>opencode.json</code>.</p>
|
|
</div>
|
|
<div class="panel" role="tabpanel" id="p-agents" aria-labelledby="tab-agents" tabindex="0">
|
|
<h4 class="panel-h">Anything that reads AGENTS.md</h4>
|
|
<p>Copy or link <code>AGENTS.md</code> (rules and skill index) into your project and use the CLI. For an MCP client <code>install</code> does not know: command <code>bunx</code> with args <code>["storebound", "mcp"]</code>.</p>
|
|
</div>
|
|
</div>
|
|
<p class="fine"><code>install</code> takes several agents at once (<code>--agent codex,cursor</code>), <code>--dry</code> to preview, and only touches its own <code>storebound</code> entry and skill folders. Re-running it is safe.</p>
|
|
</div>
|
|
<div>
|
|
<h3 class="sub-h">Every write is a dry run first</h3>
|
|
<figure class="term term-real" aria-label="storebound play products --dry: two products planned in 174 regions, nothing written">
|
|
<div class="term-bar" aria-hidden="true"><span></span><span></span><span></span><em>dry run · nothing written</em></div>
|
|
<pre><code><span class="p">$</span> storebound play products --dry
|
|
<span class="o">[play] 1/2 com.example.app.no_ads: would create · 174 regions · 2 listings (dry)</span>
|
|
<span class="o">[play] 2/2 com.example.app.gems_100: would create · 174 regions · 2 listings (dry)</span>
|
|
dry run: play products: 2 change(s); nothing written
|
|
item action detail
|
|
------------------------ ------ ---------------------------------
|
|
com.example.app.no_ads create 174 regions, 2 listings, USD 2.99
|
|
com.example.app.gems_100 create 174 regions, 2 listings, USD 0.99
|
|
<span class="o">[play] not on Google Play: com.example.app.no_ads, com.example.app.gems_100. If you created them in the last minutes, the store's list can lag a minute after creating; re-run before acting on this plan</span>
|
|
<span class="o">[play] products checked (dry run, nothing written)</span></code></pre>
|
|
<figcaption class="term-cap">Real output, <a href="img/term/products.txt">full text</a></figcaption>
|
|
</figure>
|
|
</div>
|
|
</div>
|
|
<ul class="safety">
|
|
<li><h3>Dry run everywhere</h3><p>Every write command takes <code>--dry</code>; every MCP write tool has <code>dry_run</code>, <strong>default true</strong>.</p></li>
|
|
<li><h3>The owner's buttons stay yours</h3><p>Submitting, releasing, completing a rollout, canceling a submission, deleting and creating offer codes are not MCP tools; the terminal commands ask first.</p></li>
|
|
<li><h3>Credentials stay local</h3><p>Env vars or the gitignored <code>.storebound/credentials.json</code>; the desktop app uses the system keychain. Official store APIs only, with keys you create and can revoke, never your password.</p></li>
|
|
<li><h3>Idempotent writes</h3><p>Re-running converges (MD5 checks for screenshots and previews, one Play edit per language), so a failed upload is fixed by running it again.</p></li>
|
|
<li><h3>Personal data stays local</h3><p>Tester lists, the App Review demo account, reply drafts and reports live in <code>.storebound/</code>, masked in every log line.</p></li>
|
|
<li><h3>Careful with your devices</h3><p>storebound shuts down only a simulator it booted itself, and never boots, wipes or reboots an Android emulator or device.</p></li>
|
|
</ul>
|
|
<details class="chat-more">
|
|
<summary>What a session with your agent looks like</summary>
|
|
<figure class="term chat" aria-labelledby="chat-cap">
|
|
<div class="term-bar"><span></span><span></span><span></span><em>example · tidy-lists</em></div>
|
|
<pre><code><span class="say"><span class="who-you">you<span class="sr-only">:</span></span> Add a yearly Pro plan with a 1-week free trial and a monthly plan, on both stores.</span><span class="say"><span class="who-ai">agent<span class="sr-only">:</span></span> Adding both to subscriptions in storebound.config.ts (USD 29.99 / 3.99, Turkey set by hand). Dry runs first.</span><span class="p">$</span> bunx storebound check
|
|
<span class="o">09:41:02 [check] config ok: storebound.config.ts
|
|
09:41:02 [check] subscriptions: 2 in 1 group(s)
|
|
09:41:02 [check] all good</span>
|
|
<span class="p">$</span> bunx storebound apple subscriptions --dry
|
|
<span class="o">09:41:05 [apple] reading subscription groups, subscriptions, ...
|
|
09:41:07 [apple] 1/9 create subscription group "Tidy Pro"
|
|
09:41:07 [apple] 3/9 create pro_yearly (ONE_YEAR, level 1)
|
|
09:41:07 [apple] 6/9 pro_yearly: introductory offer FREE_TRIAL P1W
|
|
09:41:07 [apple] subscriptions planned (dry run, nothing written; ...) (5s)</span>
|
|
<span class="p">$</span> bunx storebound play subscriptions --dry
|
|
<span class="o">09:41:10 [play] 1/2 pro_yearly: create, base plan yearly (P1Y) in 173 regions · intro offer intro (free)
|
|
09:41:10 [play] 2/2 pro_monthly: create, base plan monthly (P1M) in 173 regions
|
|
09:41:10 [play] subscriptions planned (dry run, nothing written) (3s)</span>
|
|
<span class="say"><span class="who-ai">agent<span class="sr-only">:</span></span> Plan matches the config. Create them on both stores?</span><span class="say"><span class="who-you">you<span class="sr-only">:</span></span> yes</span><span class="p">$</span> bunx storebound apple subscriptions && bunx storebound play subscriptions
|
|
<span class="o">09:42:31 [apple] group "Tidy Pro" created
|
|
09:42:33 [apple] 1/2 pro_yearly: created
|
|
09:42:52 [apple] subscriptions done (first ones are submitted with an app version: ...) (21s)
|
|
09:43:04 [play] pro_yearly: base plan yearly activated
|
|
09:43:05 [play] pro_yearly: offer intro created and activated
|
|
09:43:07 [play] subscriptions done (25s)</span>
|
|
<span class="p">$</span> bunx storebound validate
|
|
<span class="o"> 1. <span class="bl">BLOCKER</span> [apple] pro_yearly: no review screenshot
|
|
fix: subscriptions[].reviewScreenshot, then storebound apple subscriptions
|
|
2. warn [apple] description without a Terms of Use link: tr (3.1.2; ...)
|
|
09:43:20 [validate] 1 blocker(s)</span>
|
|
<span class="say"><span class="who-ai">agent<span class="sr-only">:</span></span> Left for you: a paywall screenshot for the reviewer (I'll upload it), the Terms link in the Turkish description, and in App Store Connect, adding the first subscriptions to the version's submission (no API; I'll walk you through it).</span><span class="say say-end"><span class="you">Submit for review · your click</span></span></code></pre>
|
|
<figcaption id="chat-cap" class="sr-only">Example session: the user asks for two subscriptions, the agent runs storebound dry runs, the user confirms, storebound creates them on both stores, and validate lists what is left.</figcaption>
|
|
</figure>
|
|
</details>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="sec" id="covers" aria-labelledby="covers-h">
|
|
<div class="wrap">
|
|
<p class="kicker">What it covers</p>
|
|
<h2 id="covers-h">Everything an app or game needs, before and after review.</h2>
|
|
<ul class="covers">
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/commands.md" target="_blank" rel="noopener"><b>Subscriptions</b><span>Apple groups and levels, Play base plans, regional prices, free trials, offers and offer codes.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/commands.md" target="_blank" rel="noopener"><b>One catalog, both stores</b><span>Consumables, non-consumables, passes and subscriptions from one list; a parity report per region.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/journey.md" target="_blank" rel="noopener"><b>Every questionnaire</b><span>Data safety through the API; IARC, target audience, App Privacy prepared and driven by your agent.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/rejections.md" target="_blank" rel="noopener"><b>Review access, account deletion</b><span>A demo account for the reviewer, kept local; Apple 5.1.1(v) and Play's delete-account page.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/store-art.md" target="_blank" rel="noopener"><b>Store art</b><span>Screenshots in every size and language, fonts for every script, app previews, A/B variants.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/commands.md" target="_blank" rel="noopener"><b>TestFlight</b><span>Groups, testers from a local CSV, What to Test in every language, build processing.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/commands.md" target="_blank" rel="noopener"><b>After launch</b><span>Crash and ANR health, review reply drafts, prices from one CSV, analytics, product page tests.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/studios.md" target="_blank" rel="noopener"><b>Many apps, release trains</b><span>A workspace for studios, one train per version across both stores, repo-local workflows.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/growth.md" target="_blank" rel="noopener"><b>Growth extras</b><span>PPP prices, Apple Ads reports, web presence (app links), ad-hoc builds, RevenueCat sync.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/api.md" target="_blank" rel="noopener"><b>Every API endpoint</b><span>Every App Store Connect and Google Play operation; GETs run, writes stay dry.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/getting-started.md" target="_blank" rel="noopener"><b>Coming from fastlane</b><span>storebound import fastlane turns deliver and supply metadata into storebound files.</span></a></li>
|
|
<li><a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/skills.md" target="_blank" rel="noopener"><b>Skills and MCP</b><span>Agent Skills for each store task and an MCP server with read and dry-run tools.</span></a></li>
|
|
</ul>
|
|
<p class="note">All of it in detail: <a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/README.md" target="_blank" rel="noopener">the docs<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>, or <code>storebound help</code>.</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="sec alt" id="start" aria-labelledby="how-h">
|
|
<div class="wrap">
|
|
<div class="sec-head">
|
|
<p class="kicker">5 · Install and start</p>
|
|
<h2 id="how-h">Five steps, from an empty folder to your Submit click.</h2>
|
|
<p class="sub">storebound lives in your app's repo. Describe the app once, add your keys, then ask your agent or run the commands yourself.</p>
|
|
</div>
|
|
|
|
<ol class="steps" data-steps>
|
|
<li class="st">
|
|
<div class="st-head" id="st-1">
|
|
<span class="st-num" aria-hidden="true">1</span>
|
|
<div class="st-t"><h3>Start in your app repo</h3><p>Pick a profile. Adds config and store files, never overwrites.</p></div>
|
|
</div>
|
|
<div class="st-panel" id="sp-1">
|
|
<div class="pv pv-term">
|
|
<div class="pv-bar"><span class="pv-k">Terminal</span><button class="copy" type="button" data-copy="bunx storebound init --profile app" aria-label="Copy step 1 command">Copy</button></div>
|
|
<pre><code><span class="p">$</span> bunx storebound init --profile app</code></pre>
|
|
</div>
|
|
<div class="pv-row">
|
|
<span class="pv-label">Profiles</span>
|
|
<span class="pill"><code>app</code></span><span class="pill"><code>subscription-app</code></span><span class="pill"><code>game</code></span>
|
|
</div>
|
|
<ul class="tree" aria-label="Files init adds">
|
|
<li><code>storebound.config.ts</code><span>ids, languages, products, Data safety</span></li>
|
|
<li><code>store/</code><span>listing text, questionnaire answers, screenshot captions</span></li>
|
|
<li><code>.storebound/</code><span>your keys, gitignored</span></li>
|
|
</ul>
|
|
<p class="pv-note">Needs <a href="https://bun.sh" target="_blank" rel="noopener">Bun<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a> 1.1+, plus <code>ffmpeg</code> for icons, screenshots and video.</p>
|
|
<div class="st-foot" hidden><span>Step 1 of 5</span><button class="st-next" type="button" data-next="1">Next: Describe the app once<span class="ic ic-caret-right" aria-hidden="true"></span></button></div>
|
|
</div>
|
|
</li>
|
|
|
|
<li class="st">
|
|
<div class="st-head" id="st-2">
|
|
<span class="st-num" aria-hidden="true">2</span>
|
|
<div class="st-t"><h3>Describe the app once</h3><p>One config for both stores, checked offline.</p></div>
|
|
</div>
|
|
<div class="st-panel" id="sp-2">
|
|
<div class="pv pv-file">
|
|
<div class="pv-bar"><span class="pv-k">storebound.config.ts</span></div>
|
|
<pre><code><span class="k">const</span> config: ShipConfig = {
|
|
app: { name: <span class="s">'My App'</span>, profile: <span class="s">'app'</span> },
|
|
languages: [
|
|
{ id: <span class="s">'en'</span>, play: <span class="s">'en-US'</span>, apple: <span class="s">'en-US'</span> },
|
|
{ id: <span class="s">'tr'</span>, play: <span class="s">'tr-TR'</span>, apple: <span class="s">'tr'</span> },
|
|
],
|
|
play: { packageName: <span class="s">'com.example.app'</span> },
|
|
apple: { bundleId: <span class="s">'com.example.app'</span> },
|
|
};</code></pre>
|
|
</div>
|
|
<div class="pv pv-term">
|
|
<div class="pv-bar"><span class="pv-k">Terminal</span><button class="copy" type="button" data-copy="bunx storebound check" aria-label="Copy step 2 command">Copy</button></div>
|
|
<pre><code><span class="p">$</span> bunx storebound check</code></pre>
|
|
</div>
|
|
<p class="pv-note">Store text lives next to it in <code>store/listing.md</code> and <code>store/appstore.md</code>, one section per language.</p>
|
|
<div class="st-foot" hidden><span>Step 2 of 5</span><button class="st-next" type="button" data-next="2">Next: Add your store keys<span class="ic ic-caret-right" aria-hidden="true"></span></button></div>
|
|
</div>
|
|
</li>
|
|
|
|
<li class="st">
|
|
<div class="st-head" id="st-3">
|
|
<span class="st-num" aria-hidden="true">3</span>
|
|
<div class="st-t"><h3>Add your store keys</h3><p>Official API keys you create and can revoke.</p></div>
|
|
</div>
|
|
<div class="st-panel" id="sp-3">
|
|
<div class="pv pv-file">
|
|
<div class="pv-bar"><span class="pv-k">.storebound/credentials.json</span><span class="pv-tag">gitignored</span><button class="copy" type="button" data-copy-target="how-creds" aria-label="Copy credentials template">Copy</button></div>
|
|
<pre id="how-creds"><code>{
|
|
<span class="s">"PLAY_SERVICE_ACCOUNT"</span>: <span class="s">"/keys/play.json"</span>,
|
|
<span class="s">"ASC_KEY_ID"</span>: <span class="s">"KEY_ID"</span>,
|
|
<span class="s">"ASC_ISSUER_ID"</span>: <span class="s">"ISSUER_ID"</span>,
|
|
<span class="s">"ASC_P8"</span>: <span class="s">"/keys/AuthKey_KEY_ID.p8"</span>
|
|
}</code></pre>
|
|
</div>
|
|
<p class="pv-note">A Play service account and an App Store Connect API key. Env vars work too. Never your password, never committed.</p>
|
|
<div class="st-foot" hidden><span>Step 3 of 5</span><button class="st-next" type="button" data-next="3">Next: Connect your agent, ask in plain words<span class="ic ic-caret-right" aria-hidden="true"></span></button></div>
|
|
</div>
|
|
</li>
|
|
|
|
<li class="st">
|
|
<div class="st-head" id="st-4">
|
|
<span class="st-num" aria-hidden="true">4</span>
|
|
<div class="st-t"><h3>Connect your agent, ask in plain words</h3><p>It runs dry runs first and walks you through console-only forms.</p></div>
|
|
</div>
|
|
<div class="st-panel" id="sp-4">
|
|
<div class="pv pv-term">
|
|
<div class="pv-bar"><span class="pv-k">Terminal</span><button class="copy" type="button" data-copy="bunx storebound install --agent claude" aria-label="Copy step 4 command">Copy</button></div>
|
|
<pre><code><span class="p">$</span> bunx storebound install --agent claude</code></pre>
|
|
</div>
|
|
<p class="pv-note">Or <code>codex</code>, <code>cursor</code>, <code>copilot</code>, <code>gemini</code>, <code>windsurf</code>, <code>opencode</code> (<a href="#agents">per tool</a>). No agent? Every step is a CLI command too.</p>
|
|
<ul class="asks2" aria-label="Things to ask">
|
|
<li><span>Set up my in-app products on both stores</span><button class="copy" type="button" data-copy="Set up my in-app products on both stores" aria-label="Copy prompt: Set up my in-app products on both stores">Copy</button></li>
|
|
<li><span>Make store screenshots in 14 languages</span><button class="copy" type="button" data-copy="Make store screenshots in 14 languages" aria-label="Copy prompt: Make store screenshots in 14 languages">Copy</button></li>
|
|
<li><span>What still blocks my submission?</span><button class="copy" type="button" data-copy="What still blocks my submission?" aria-label="Copy prompt: What still blocks my submission?">Copy</button></li>
|
|
</ul>
|
|
<div class="st-foot" hidden><span>Step 4 of 5</span><button class="st-next" type="button" data-next="4">Next: Zero blockers, then you submit<span class="ic ic-caret-right" aria-hidden="true"></span></button></div>
|
|
</div>
|
|
</li>
|
|
|
|
<li class="st">
|
|
<div class="st-head" id="st-5">
|
|
<span class="st-num" aria-hidden="true">5</span>
|
|
<div class="st-t"><h3>Zero blockers, then you submit</h3><p>A read-only check of both stores, in fix order.</p></div>
|
|
</div>
|
|
<div class="st-panel" id="sp-5">
|
|
<div class="pv pv-term">
|
|
<div class="pv-bar"><span class="pv-k">Terminal</span><button class="copy" type="button" data-copy="bunx storebound validate" aria-label="Copy step 5 command">Copy</button></div>
|
|
<pre><code><span class="p">$</span> bunx storebound validate</code></pre>
|
|
</div>
|
|
<ol class="gate" aria-label="The last step">
|
|
<li><b>Blockers, per store, in fix order</b><span>build, encryption, metadata, screenshots, review details, prices, products</span></li>
|
|
<li><b>Fix, run again, until zero</b><span>read-only, so it is safe to run as often as you like</span></li>
|
|
<li class="gate-you"><b>You press Submit for review</b><span>and Send changes for review on Play. Never the agent.</span></li>
|
|
</ol>
|
|
<div class="st-foot" hidden><span>Step 5 of 5</span><a class="st-next" href="#download">Get the desktop app too<span class="ic ic-caret-right" aria-hidden="true"></span></a></div>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
|
|
<div class="for">
|
|
<h3 class="for-h" id="who-h">Built for</h3>
|
|
<ul class="for-list" aria-labelledby="who-h">
|
|
<li>
|
|
<span class="for-ic" aria-hidden="true"><span class="ic ic-user"></span></span>
|
|
<div><b>Indie developers</b><p>One app or a handful: the store chores without a week of console tabs.</p></div>
|
|
</li>
|
|
<li>
|
|
<span class="for-ic" aria-hidden="true"><span class="ic ic-squares-four"></span></span>
|
|
<div><b>Studios shipping many titles</b><p>One config per title, the same checks and store art pipeline every time.</p></div>
|
|
</li>
|
|
<li>
|
|
<span class="for-ic" aria-hidden="true"><span class="ic ic-briefcase"></span></span>
|
|
<div><b>Agencies</b><p>Reviewable config in the client's repo. Their keys, their Submit.</p></div>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="sec" id="faq" aria-labelledby="faq-h">
|
|
<div class="wrap faq-in">
|
|
<div class="faq-side">
|
|
<p class="kicker">FAQ</p>
|
|
<h2 id="faq-h">Questions, answered.</h2>
|
|
<p class="sub">The short version. The docs have the long one, with every command and what each store allows through its API.</p>
|
|
<p class="faq-links">
|
|
<a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/README.md" target="_blank" rel="noopener">Read the docs<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
<a href="https://git.crt.fyi/CRT/storebound/issues" target="_blank" rel="noopener">Ask on the issue tracker<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
</p>
|
|
</div>
|
|
<div class="faq">
|
|
<details open>
|
|
<summary><h3>Do I need an AI agent?</h3></summary>
|
|
<div class="faq-a"><p>No. Every step is a CLI command too (<code>bunx storebound --help</code>), and the skills are Markdown checklists you can read yourself for the console-only forms. The agent integrations are optional.</p></div>
|
|
</details>
|
|
<details>
|
|
<summary><h3>Will it press Submit for me?</h3></summary>
|
|
<div class="faq-a"><p>No, never. Submitting, releasing, completing a rollout and deleting are not MCP tools, and the terminal commands for them ask first. storebound gets you to zero blockers; you press Submit for review on the App Store and Send changes for review on Play.</p></div>
|
|
</details>
|
|
<details>
|
|
<summary><h3>Is it only for games?</h3></summary>
|
|
<div class="faq-a"><p>No. <code>--profile app</code> (the default) and <code>--profile subscription-app</code> bring templates for utility and subscription apps: accounts, account deletion, demo accounts for review, subscriptions on both stores and a calm screenshot style. <code>--profile game</code> is for free-to-play games with ads and in-app purchases.</p></div>
|
|
</details>
|
|
<details>
|
|
<summary><h3>Where do my keys go? Is my store account safe?</h3></summary>
|
|
<div class="faq-a"><p>In environment variables or <code>.storebound/credentials.json</code>, which <code>init</code> gitignores. storebound uses the official store APIs with keys you create and can revoke, never your password. Use an App Store Connect key with the App Manager role and a Play service account limited to your app. Every write is a dry run until you say yes.</p></div>
|
|
</details>
|
|
<details>
|
|
<summary><h3>My app is Unity, Godot, Flutter, React Native or native.</h3></summary>
|
|
<div class="faq-a"><p>Fine. The store commands work on build numbers and listing files, not engines. Screenshots and clips come from a web build, the iOS Simulator, an Android emulator or device, or a folder of your own frames.</p></div>
|
|
</details>
|
|
<details>
|
|
<summary><h3>Does it cost anything? Any telemetry?</h3></summary>
|
|
<div class="faq-a"><p>It is free and open source under the MIT license. No account, no sign-up, no telemetry: it only talks to the Google Play and App Store Connect APIs with your keys. You still need your own Play Console and Apple Developer accounts.</p></div>
|
|
</details>
|
|
<details>
|
|
<summary><h3>Can I use it for many apps?</h3></summary>
|
|
<div class="faq-a"><p>Yes. Each app or game gets its own config in its own repo, so the same checks and store art pipeline run for every title. If you ship many similar apps, <code>storebound originality --against <folders></code> compares one against the others offline (listing text, name, icon and screenshot perceptual hashes, project structure) for Apple's spam guideline (4.3) and Play's repetitive content policy, and exits with 1 on a near-duplicate. <code>storebound scan --originality</code> runs the same check as part of the scan, and the <code>portfolio-originality</code> skill walks your agent through the fixes.</p></div>
|
|
</details>
|
|
<details>
|
|
<summary><h3>Does it run on Windows or Linux?</h3></summary>
|
|
<div class="faq-a"><p>It runs wherever Bun 1.1+ runs; it does not run on plain Node. The store commands are API calls. Capturing from the iOS Simulator needs a Mac with Xcode; a web build, an Android emulator or a folder of frames works anywhere. Icons, screenshots and video also need <code>ffmpeg</code>.</p></div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<footer class="foot">
|
|
<div class="wrap foot-in">
|
|
<div>
|
|
<a class="brand" href="#top" aria-label="storebound, back to top">
|
|
<img class="mark" src="img/logo-mark.png?v=2" srcset="img/logo-mark.png?v=2 1x, img/logo-mark@2x.png?v=2 2x" width="28" height="28" alt="storebound logo">
|
|
<span>storebound</span>
|
|
</a>
|
|
<p class="fine">MIT · made by CRTkafa · not affiliated with Google or Apple.<br>App Store is a trademark of Apple Inc.; Google Play is a trademark of Google LLC.</p>
|
|
</div>
|
|
<nav aria-label="Footer">
|
|
<a href="https://git.crt.fyi/CRT/storebound" target="_blank" rel="noopener">git repo<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
<a href="https://www.npmjs.com/package/storebound" target="_blank" rel="noopener">npm<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
<a href="https://git.crt.fyi/CRT/storebound/src/branch/main/docs/README.md" target="_blank" rel="noopener">docs<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
<a href="https://git.crt.fyi/CRT/storebound/src/branch/main/CHANGELOG.md" target="_blank" rel="noopener">CHANGELOG<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
<a href="https://git.crt.fyi/CRT/storebound/src/branch/main/SECURITY.md" target="_blank" rel="noopener">SECURITY<span class="ext" aria-hidden="true">↗</span><span class="sr-only"> (opens in a new tab)</span></a>
|
|
</nav>
|
|
</div>
|
|
</footer>
|
|
</body>
|
|
</html>
|