init --profile app|game|subscription-app (default app): per-profile config, declarations, App Privacy, Data safety, listing and screenshot copy

This commit is contained in:
CRTkafa 2026-10-11 01:47:09 +08:00
parent 44699eac8c
commit 7f0bb7c2dd
15 changed files with 872 additions and 5 deletions

View file

@ -1,12 +1,18 @@
// Copies the templates into the current project without overwriting anything, and makes sure .storebound/ is ignored. // Copies the templates into the current project without overwriting anything, and makes sure .storebound/ is ignored.
// storebound init [--dir .] // storebound init [--dir .] [--profile app|game|subscription-app]
// Profiles (default app): app = utility / productivity app with an optional account and analytics, no ads;
// game = free-to-play game with ads and in-app purchases (the templates storebound shipped before 0.5.0);
// subscription-app = accounts plus auto-renewable subscriptions. Each profile has its own config, declarations,
// App Privacy answers, Data safety section, listing copy and screenshot copy; every answer is a starting point to edit.
// Lookup per file: templates/profiles/<profile>/<file>, then the profile it builds on, then templates/<file>.
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path'; import { dirname, join, resolve } from 'node:path';
import type { Args } from './lib/args'; import type { Args } from './lib/args';
import { type AppProfile, PROFILES } from './lib/config';
import { log } from './lib/log'; import { log } from './lib/log';
const T = join(import.meta.dir, '..', 'templates'); const T = join(import.meta.dir, '..', 'templates');
const FILES: [string, string][] = [ export const FILES: [string, string][] = [
['storebound.config.ts', 'storebound.config.ts'], ['storebound.config.ts', 'storebound.config.ts'],
['listing.md', 'store/listing.md'], ['listing.md', 'store/listing.md'],
['appstore.md', 'store/appstore.md'], ['appstore.md', 'store/appstore.md'],
@ -18,13 +24,29 @@ const FILES: [string, string][] = [
['testers.csv', '.storebound/testers.csv'], ['testers.csv', '.storebound/testers.csv'],
]; ];
/** Template folders a profile reads from, most specific first. game = the root templates. */
const CHAIN: Record<AppProfile, string[]> = {
'game': [T],
'app': [join(T, 'profiles', 'app'), T],
'subscription-app': [join(T, 'profiles', 'subscription-app'), join(T, 'profiles', 'app'), T],
};
/** Source file of one template for a profile. Pure apart from existsSync. */
export function templateFor(profile: AppProfile, name: string): string {
for (const dir of CHAIN[profile]) if (existsSync(join(dir, name))) return join(dir, name);
throw new Error(`template ${name} not found`);
}
export async function run(args: Args): Promise<void> { export async function run(args: Args): Promise<void> {
const root = resolve(args.str('dir', '.')); const root = resolve(args.str('dir', '.'));
const profile = args.str('profile', 'app') as AppProfile;
if (!PROFILES.includes(profile)) throw new Error(`--profile ${profile}: one of ${PROFILES.join(', ')}`);
log('init', `profile ${profile} (templates are starting points: edit every answer to match your app)`);
for (const [from, to] of FILES) { for (const [from, to] of FILES) {
const dest = join(root, to); const dest = join(root, to);
if (existsSync(dest)) { log('init', `keep ${to} (exists)`); continue; } if (existsSync(dest)) { log('init', `keep ${to} (exists)`); continue; }
mkdirSync(dirname(dest), { recursive: true }); mkdirSync(dirname(dest), { recursive: true });
copyFileSync(join(T, from), dest); copyFileSync(templateFor(profile, from), dest);
log('init', `create ${to}`); log('init', `create ${to}`);
} }
const gi = join(root, '.gitignore'); const gi = join(root, '.gitignore');
@ -34,5 +56,5 @@ export async function run(args: Args): Promise<void> {
else writeFileSync(gi, '# storebound: credentials, fonts, temp files\n.storebound/\n'); else writeFileSync(gi, '# storebound: credentials, fonts, temp files\n.storebound/\n');
log('init', '.gitignore: added .storebound/'); log('init', '.gitignore: added .storebound/');
} }
log('init', 'next: edit storebound.config.ts and .storebound/credentials.json, then `storebound check`'); log('init', 'next: edit storebound.config.ts, store/*.md and .storebound/credentials.json, then `storebound check`');
} }

View file

@ -0,0 +1,29 @@
# Example App · App Store "App Privacy" answers
App Store Connect > your app > App Privacy. **Web UI only, there is no API.** Fill it from this table, then press
**Publish** (saving alone does not publish it). Keep it consistent with the privacy policy and the Play Data safety
answers (`dataSafety` in `storebound.config.ts`). **Edit to match your app.**
Typical utility / productivity app: optional account (email), content synced to your backend, an analytics SDK
acting as your processor, no ads, no tracking.
**Do you or your third-party partners collect data from this app?** Yes
**Tracking**: No (no ad SDK, no data broker, nothing combined with other companies' data for advertising). If that
changes, add the App Tracking Transparency prompt first.
| data type | collected | used to track | linked to the user | purposes |
|---|---|---|---|---|
| Contact Info > Email Address | yes (account) | no | yes | App Functionality |
| Identifiers > User ID | yes (account id) | no | yes | App Functionality, Analytics |
| User Content > Other User Content (lists, notes) | yes (synced) | no | yes | App Functionality |
| Usage Data > Product Interaction | yes (analytics) | no | yes if tied to the account, else no | Analytics |
| Diagnostics > Crash Data | yes | no | no | App Functionality, Analytics |
| Diagnostics > Performance Data | yes | no | no | Analytics |
"Linked to you" is **Yes** for what you store under the account. Not collected (typical): Location, Health and
Fitness, Financial Info, Contacts, Photos or Videos (unless people attach them), Browsing and Search History,
Purchases (until you sell something), Sensitive Info.
Per data type the UI asks, in order: purposes > "Is this data linked to the user's identity?" > "Do you or your
third-party partners use this data for tracking purposes?".

View file

@ -0,0 +1,67 @@
# Example App · App Store listing copy
Source for `storebound apple listing`. One `## <ASC locale>` section per locale (the first word is the locale code:
`en-US`, `tr`, `de-DE`, `ja`, `zh-Hans`, `ar-SA`, ...), one fenced block per `### <field>`, field names exactly:
`name` (optional, defaults to `app.name`), `subtitle` (30), `promotionalText` (170), `keywords` (100 bytes, comma
separated, no spaces after commas, do not repeat the app name), `description` (4000), `whatsNew` (optional; not allowed
on the first version). Shared values (privacy policy, support and marketing URL) come from `storebound.config.ts`.
---
## en-US · English (U.S.)
### subtitle
```
Lists and notes, in sync
```
### promotionalText
```
One or two sentences you can change any time without a new version.
```
### keywords
```
todo,checklist,groceries,planner,notes,shared,family,organizer,tasks,sync
```
### description
```
First paragraph: the job the app does and why it is easier with it.
FEATURES
• Lists and notes that sync across your devices
• Share a list with family or friends
• Export your data or delete your account in Settings, any time
Free to use. An account is optional and only needed for sync and sharing.
```
## tr · Türkçe
### subtitle
```
Listeler ve notlar, eşitli
```
### promotionalText
```
Yeni sürüm göndermeden istediğin zaman değiştirebileceğin bir iki cümle.
```
### keywords
```
yapılacaklar,kontrol,market,planlayıcı,not,paylaşım,aile,düzen,görev
```
### description
```
İlk paragraf: uygulamanın gördüğü iş ve onunla neden daha kolay olduğu.
ÖZELLİKLER
• Tüm cihazlarında eşitlenen listeler ve notlar
• Bir listeyi ailen ya da arkadaşlarınla paylaş
• Verilerini dışa aktar ya da hesabını Ayarlar'dan istediğin an sil
Ücretsiz. Hesap isteğe bağlıdır; yalnızca eşitleme ve paylaşım için gerekir.
```

View file

@ -0,0 +1,57 @@
# Example App · store declarations (answers to copy into the consoles)
Starting answers for a typical **utility / productivity app**: an optional account (email + password, or sign in
with a provider), an analytics SDK acting as your processor, data synced to your own backend, no ads, no chat with
strangers, general audience. **Edit every line to match your app**: these are your statements to the stores. Everything
marked **UI** has no API: Play Console > your app > Policy and programs > **App content**, and App Store Connect for
Apple. Data safety (Play) has an API: `storebound play data-safety`.
## Google Play · App content (UI)
| declaration | answer |
|---|---|
| Privacy policy | your policy URL (same one in the listing and inside the app) |
| App access | "All or some functionality is restricted": add the demo account (username, password) and the steps from `reviewAccess.instructions`; credentials must work at all times, no 2FA / OTP (review-access skill) |
| Ads | No, my app does not contain ads |
| Content rating | IARC questionnaire, below |
| Target audience and content | 18 and over (or 13+ if teens use it); "Could your app unintentionally appeal to children?" No (only if true) |
| News app | No |
| Data safety | `storebound play data-safety`, then check the summary; with accounts: the delete-account URL (`accounts.deletionUrl`) |
| Government apps | No |
| Financial features | My app doesn't provide any financial features |
| Health apps | No health features |
| Advertising ID | No (unless an SDK in your app reads it: check the SDK docs and your merged manifest) |
| Store settings | App, category (Productivity, Tools, ...); contact email + website |
## Account deletion (account-deletion skill)
- In the app: Settings > Account > Delete account deletes the account and its data (not only "deactivate").
- On the web: `accounts.deletionUrl` lets people request deletion without the app; Play asks for it in Data safety.
- With subscriptions: say that deleting the account does not cancel the store subscription, and how to cancel.
## IARC content rating (Play UI; Apple has its own form, `storebound apple appinfo`)
Category: not a game (e.g. Utility, Productivity, Communication, or Social Networking if people share content with
others). Typical answers for a lists / notes app:
| topic | answer |
|---|---|
| Violence, sexuality, language, controlled substances | No |
| Gambling (simulated / real money) | No |
| Users interact or exchange content | Yes only if people can share with others outside their own invitations; then also moderation and reporting |
| Shares location with other users | No |
| Digital purchases | No (Yes once you sell IAPs or subscriptions) |
| Unrestricted internet / browser | No |
Expected: PEGI 3, ESRB Everyone, IARC 3+. Apple age rating 4+ (all content questions None) unless users share
content publicly.
## App Store (UI-only parts)
- App record: App Store Connect > Apps > + > New App. No API.
- App Privacy: `app-privacy.md`, then **Publish**.
- App Review information: `storebound apple review-details` sends the contact, the demo account (from
`.storebound/credentials.json`, masked in the log) and the notes.
- Export compliance (export-compliance skill): `ITSAppUsesNonExemptEncryption = NO` only if true.
- Guideline 5.1.1: the privacy policy link must also be reachable **inside the app**; 5.1.1(v): account deletion
inside the app.

View file

@ -0,0 +1,61 @@
# Example App · Google Play listing copy
Source for `storebound play listing` (title, short + full description) and `storebound play release` (release notes).
Rules the parser follows:
- every `## ...` section with a `<!-- play: <codes> -->` line is a listing; one section can serve several Play codes
- under it, `### ...` headings that contain **title** (optional, defaults to `app.name`), **short**, **full** and
**what's new**, each followed by one fenced block
- limits (checked before anything is sent): title 30, short 80, full 4000, what's new 500 characters
Only claim what the app really does. No prices, no "best / #1", no other apps' names.
---
## English (en-US)
<!-- play: en-US -->
### Play short description (max 80)
```
What the app does for people, in one plain line
```
### Play full description (max 4000)
```
First paragraph: the job the app does and why it is easier with it, in two or three sentences.
FEATURES
• Lists and notes that sync across your devices
• Share a list with family or friends
• Export your data or delete your account in Settings, any time
Free to use. An account is optional and only needed for sync and sharing.
```
### What's new (max 500)
```
First release. Thanks for trying it!
```
## Türkçe (tr-TR)
<!-- play: tr-TR -->
### Play kısa açıklama · short (max 80)
```
Uygulamanın insanlar için ne yaptığını anlatan sade bir cümle
```
### Play tam açıklama · full (max 4000)
```
İlk paragraf: uygulamanın gördüğü iş ve onunla neden daha kolay olduğu, iki üç cümlede.
ÖZELLİKLER
• Tüm cihazlarında eşitlenen listeler ve notlar
• Bir listeyi ailen ya da arkadaşlarınla paylaş
• Verilerini dışa aktar ya da hesabını Ayarlar'dan istediğin an sil
Ücretsiz. Hesap isteğe bağlıdır; yalnızca eşitleme ve paylaşım için gerekir.
```
### Yenilikler · what's new (max 500)
```
İlk sürüm. Denediğin için teşekkürler!
```

View file

@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Privacy Policy · Example Studio</title>
<!--
One privacy policy for your apps (profile "app": accounts, synced content, analytics, no ads), with a per-app
services table. Host it at a stable URL (https://example.com/privacy) and use that URL in both store listings, in
storebound.config.ts and inside each app (Settings > Privacy; App Store guideline 5.1.1). Replace every "Example"
value and remove services you do not use. Update the date and the table whenever an app starts collecting something
new, then the store declarations. This is a template, not legal advice.
-->
<style>
:root { color-scheme: light dark; --fg: #1b1c20; --bg: #ffffff; --muted: #5d6170; --line: #e3e5ea; --accent: #2563eb; }
@media (prefers-color-scheme: dark) { :root { --fg: #eceef3; --bg: #15161a; --muted: #a3a8b6; --line: #2b2d35; --accent: #7aa2ff; } }
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.6 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
main { max-width: 760px; margin: 0 auto; padding: 32px 16px 64px; }
h1 { font-size: 1.8rem; margin: 0 0 4px; } h2 { font-size: 1.2rem; margin-top: 2em; }
p.meta { color: var(--muted); margin: 0 0 24px; }
a { color: var(--accent); }
.table { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: .95rem; }
th, td { text-align: left; vertical-align: top; padding: 8px 10px; border-bottom: 1px solid var(--line); }
th { font-weight: 600; }
nav a { margin-right: 12px; }
</style>
</head>
<body>
<main>
<nav><a href="#en">English</a><a href="#tr">Türkçe</a></nav>
<section id="en">
<h1>Privacy Policy</h1>
<p class="meta">Example Studio · applies to all our apps · last updated 2026-01-01</p>
<p>We collect what an app needs to work for you, never sell personal data and show no ads. This page explains what
each app collects, why, and what you can do about it.</p>
<h2>What our apps use</h2>
<div class="table"><table>
<tr><th>App</th><th>Service</th><th>What and why</th><th>Your choice</th></tr>
<tr><td>Example App</td><td>Account (our servers)</td><td>Email address, account id and the lists and notes you create: to sign you in and sync your content between your devices.</td><td>The account is optional; export or delete it in Settings &gt; Account</td></tr>
<tr><td>Example App</td><td>Analytics (e.g. a privacy-friendly analytics SDK)</td><td>Random install id, screens used, device model, OS version: to improve the app.</td><td>Settings &gt; Analytics off</td></tr>
<tr><td>Example App</td><td>Crash reporting</td><td>Crash logs and device model: to fix crashes.</td><td>-</td></tr>
<tr><td>Example App</td><td>App stores</td><td>Purchases and subscriptions are handled by Google Play / the App Store; we never see payment details.</td><td>-</td></tr>
</table></div>
<h2>Deleting your account</h2>
<p>In the app: Settings &gt; Account &gt; Delete account. Without the app: <a href="https://example.com/delete-account">example.com/delete-account</a>.
We delete your account and its content within 30 days; backups roll over within 90 days. A store subscription is
not cancelled by deleting the account: cancel it in your Google Play or App Store subscriptions.</p>
<h2>Children</h2>
<p>Our apps are not directed at children under 13. We do not knowingly collect personal data from children. If you
believe a child sent us data, contact us and we will delete it.</p>
<h2>Retention and your rights</h2>
<p>We keep account data while the account exists and analytics data for up to 24 months. You can ask for a copy or
the deletion of your data by writing to <a href="mailto:privacy@example.com">privacy@example.com</a>; we answer
within 30 days.</p>
<h2>Security</h2>
<p>All network traffic of our apps uses HTTPS; passwords are stored only as salted hashes.</p>
<h2>Changes</h2>
<p>When an app starts collecting something new we update this page and its date first.</p>
<h2>Contact</h2>
<p>Example Studio · <a href="mailto:privacy@example.com">privacy@example.com</a></p>
</section>
<section id="tr" lang="tr">
<h1>Gizlilik Politikası</h1>
<p class="meta">Example Studio · tüm uygulamalarımız için geçerlidir · son güncelleme 2026-01-01</p>
<p>Yalnızca uygulamanın sizin için çalışması gereken verileri topluyoruz, kişisel verileri asla satmıyoruz ve reklam
göstermiyoruz. Yukarıdaki tablo her uygulamanın neyi neden topladığını gösterir.</p>
<h2>Hesabınızı silmek</h2>
<p>Uygulamada: Ayarlar &gt; Hesap &gt; Hesabı sil. Uygulama olmadan: <a href="https://example.com/delete-account">example.com/delete-account</a>.
Mağaza aboneliği hesap silinince iptal olmaz; Google Play veya App Store aboneliklerinizden iptal edin.</p>
<h2>Haklarınız</h2>
<p>Verilerinizin bir kopyasını veya silinmesini <a href="mailto:privacy@example.com">privacy@example.com</a> adresine
yazarak isteyebilirsiniz; 30 gün içinde yanıtlıyoruz.</p>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,14 @@
{
"en": {
"home": { "eyebrow": "Example App", "title": ["Everything in order,", "in one calm place"], "sub": "Lists and notes, synced on all your devices." },
"detail": { "eyebrow": "Get it done", "title": ["See what is left", "at a glance"], "sub": "Tap to tick, swipe to reorder." },
"share": { "eyebrow": "Together", "title": ["Share a list,", "plan together"], "sub": "Invite people by link and edit live." },
"privacy": { "eyebrow": "Your data", "title": ["Private by default,", "yours to export"], "sub": "Export your data or delete your account in Settings." }
},
"tr": {
"home": { "eyebrow": "Example App", "title": ["Her şey düzenli,", "tek sakin bir yerde"], "sub": "Listeler ve notlar, tüm cihazlarında eşitlenir." },
"detail": { "eyebrow": "Bitir", "title": ["Kalanları", "bir bakışta gör"], "sub": "Dokun işaretle, sürükle sırala." },
"share": { "eyebrow": "Birlikte", "title": ["Listeyi paylaş,", "birlikte planla"], "sub": "Bağlantıyla davet et, canlı düzenleyin." },
"privacy": { "eyebrow": "Verilerin", "title": ["Varsayılan olarak gizli,", "dışa aktarması senin"], "sub": "Verilerini dışa aktar ya da hesabını Ayarlar'dan sil." }
}
}

View file

@ -0,0 +1,130 @@
// storebound config for one app (profile "app": a utility / productivity app with an optional account and an
// analytics SDK, no ads). Every answer below is a starting point: edit it to match your app. Credentials NEVER go
// here (env vars or .storebound/credentials.json, gitignored). Paths are relative to this file.
// The type import is optional: it only works when storebound is installed in the project (bun add -d storebound).
import type { ShipConfig } from 'storebound';
const config: ShipConfig = {
app: {
name: 'Example App',
profile: 'app',
copyright: '2026 Example Studio',
privacyPolicyUrl: 'https://example.com/privacy',
// termsUrl: 'https://example.com/terms', // Terms of Use; required once you sell subscriptions
supportUrl: 'https://example.com/support',
marketingUrl: 'https://example.com',
website: 'https://example.com',
contactEmail: 'support@example.com',
},
// Accounts (account-deletion skill). Remove this block if the app has no sign-up / sign-in at all.
// Apple 5.1.1(v): deletion must start inside the app. Google Play: in the app AND on a public web page.
accounts: {
enabled: true,
deletionUrl: 'https://example.com/delete-account', // public page, no login needed to request deletion
inApp: 'Settings > Account > Delete account', // goes into the App Review notes
},
// Sign-in for store reviewers (review-access skill). The demo account itself lives ONLY in
// .storebound/credentials.json ("reviewDemoAccount") or REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD.
reviewAccess: {
signInRequired: true, // false if every feature works without signing in
instructions: 'Sign in with the demo account. Sample data is already there; nothing needs a second device.',
},
// id = your folder / copy key; play = Play language code(s); apple = App Store Connect locale
languages: [
{ id: 'en', play: 'en-US', apple: 'en-US' },
{ id: 'tr', play: 'tr-TR', apple: 'tr' },
],
play: {
packageName: 'com.example.app',
defaultLanguage: 'en-US',
listing: 'store/listing.md',
icon: 'store/icons/play-512.png',
},
apple: {
bundleId: 'com.example.app',
listing: 'store/appstore.md',
// Export compliance (export-compliance skill): decide it, then uncomment. false ONLY if the app uses no encryption
// beyond the OS (HTTPS through the system, standard sign-in).
// usesNonExemptEncryption: false,
// ipad: true,
info: {
categories: { primary: 'PRODUCTIVITY', secondary: 'UTILITIES' },
ageRating: {}, // every content question NONE / false; change what applies (user-generated content, web access ...)
contentRights: 'DOES_NOT_USE_THIRD_PARTY_CONTENT',
free: true,
reviewNotes: [
'Example App keeps lists and notes in sync across devices. An account is optional on the first screen.',
'Privacy policy and Terms: Settings > About.',
].join('\n\n'),
},
},
// In-app purchases (optional): a one-time "Pro" unlock, for example. Subscriptions: see the subscription-app profile
// (storebound init --profile subscription-app) or the store-subscriptions skill.
// products: [{ id: 'com.example.app.pro', type: 'non_consumable', usd: 4.99,
// listings: { en: { title: 'Pro', description: 'Unlimited lists and themes.' } }, reviewScreenshot: 'store/iap-review/pro.png' }],
// Google Play Data safety. Types not listed = not collected. Typical app with accounts + an analytics SDK acting as
// your processor (collected, not shared). Edit to match what your app and its SDKs really do.
dataSafety: {
collectsData: true,
encryptedInTransit: true,
deletionRequest: true,
// Sign-up methods. Google does not publish these ids: compare with Play Console > App content > Data safety >
// Export to CSV before the first import. The delete-account URL question that follows: add it under `extra` with
// the exact id from that export, e.g. { question: '<id from the export>', value: 'https://example.com/delete-account' }.
accountCreation: ['PSL_ACM_USER_ID_PASSWORD'],
data: {
PSL_EMAIL: { shared: false, collect: ['functionality', 'account'] },
PSL_USER_ACCOUNT: { shared: false, collect: ['functionality', 'account'] },
PSL_USER_GENERATED_CONTENT: { shared: false, collect: ['functionality'] },
PSL_USER_INTERACTION: { shared: false, collect: ['analytics'] },
PSL_CRASH_LOGS: { shared: false, collect: ['analytics'] },
PSL_PERFORMANCE_DIAGNOSTICS: { shared: false, collect: ['analytics'] },
PSL_DEVICE_ID: { shared: false, collect: ['analytics'] },
},
},
renders: 'store/renders',
shots: {
style: 'app', // calm gradient, device frame, headline + subline (game: sparkles and badges)
copy: 'store/shots/copy.json',
frames: 'store/shots/frames', // <lang>/phone/<name>.png, <lang>/tablet/<name>.png
out: 'store/renders',
// fonts: { title: 'Inter', body: 'Inter' }, // the app style default
scenes: [
{ id: 'home', palette: 'mist', frames: [{ src: 'home.png' }] },
{ id: 'detail', palette: 'mint', frames: [{ src: 'detail.png' }] },
{ id: 'share', palette: 'sky', frames: [{ src: 'share.png' }] },
{ id: 'privacy', palette: 'slate', frames: [{ src: 'settings.png' }] },
],
feature: { frame: 'home.png', palette: 'mist' },
// Raw frames from a web build, the iOS Simulator (ios-sim) or an Android emulator (android-emu): sign in to a demo
// account with realistic sample data, never real user data (store-screenshots skill).
capture: {
baseUrl: 'http://127.0.0.1:5173',
query: 'lang={lang}&demo=1',
shots: {
'home': { path: '/', delayMs: 1000, device: 'both' },
'detail': { path: '/lists/1', delayMs: 1000, device: 'both' },
'share': { path: '/lists/1/share', delayMs: 1000, device: 'both' },
'settings': { path: '/settings', delayMs: 1000, device: 'both' },
},
},
},
icons: {
composite: 'store/icon/icon.png',
foreground: 'store/icon/foreground.png',
background: 'store/icon/background.png',
out: 'store/icons',
},
};
export default config;

View file

@ -0,0 +1,30 @@
# Example App · App Store "App Privacy" answers
App Store Connect > your app > App Privacy. **Web UI only, there is no API.** Fill it from this table, then press
**Publish** (saving alone does not publish it). Keep it consistent with the privacy policy and the Play Data safety
answers (`dataSafety` in `storebound.config.ts`). **Edit to match your app.**
Typical subscription app: account (email), content synced to your backend, auto-renewable subscriptions checked by
your server, an analytics SDK acting as your processor, no ads, no tracking.
**Do you or your third-party partners collect data from this app?** Yes
**Tracking**: No (no ad SDK, no data broker, nothing combined with other companies' data for advertising). If that
changes, add the App Tracking Transparency prompt first.
| data type | collected | used to track | linked to the user | purposes |
|---|---|---|---|---|
| Contact Info > Email Address | yes (account) | no | yes | App Functionality |
| Identifiers > User ID | yes (account id) | no | yes | App Functionality, Analytics |
| User Content > Other User Content (lists, notes) | yes (synced) | no | yes | App Functionality |
| Usage Data > Product Interaction | yes (analytics) | no | yes if tied to the account, else no | Analytics |
| Purchases > Purchase History (subscription status) | yes | no | yes | App Functionality |
| Diagnostics > Crash Data | yes | no | no | App Functionality, Analytics |
| Diagnostics > Performance Data | yes | no | no | Analytics |
"Linked to you" is **Yes** for what you store under the account. Not collected (typical): Location, Health and
Fitness, Financial Info, Contacts, Photos or Videos (unless people attach them), Browsing and Search History,
Sensitive Info.
Per data type the UI asks, in order: purposes > "Is this data linked to the user's identity?" > "Do you or your
third-party partners use this data for tracking purposes?".

View file

@ -0,0 +1,73 @@
# Example App · App Store listing copy
Source for `storebound apple listing`. One `## <ASC locale>` section per locale (the first word is the locale code:
`en-US`, `tr`, `de-DE`, `ja`, `zh-Hans`, `ar-SA`, ...), one fenced block per `### <field>`, field names exactly:
`name` (optional, defaults to `app.name`), `subtitle` (30), `promotionalText` (170), `keywords` (100 bytes, comma
separated, no spaces after commas, do not repeat the app name), `description` (4000), `whatsNew` (optional; not allowed
on the first version). Shared values (privacy policy, support and marketing URL) come from `storebound.config.ts`.
---
## en-US · English (U.S.)
### subtitle
```
Lists and notes, in sync
```
### promotionalText
```
One or two sentences you can change any time without a new version.
```
### keywords
```
todo,checklist,groceries,planner,notes,shared,family,organizer,tasks,sync
```
### description
```
First paragraph: the job the app does and why it is easier with it.
FEATURES
• Lists and notes that sync across your devices
• Share a list with family or friends
• Export your data or delete your account in Settings, any time
Free to use with up to 3 lists. Example Pro unlocks unlimited lists, sharing and themes: yearly with a 1-week free trial, or monthly. Payment is charged to your store account; the subscription renews automatically unless cancelled at least 24 hours before the end of the period, and you can manage or cancel it in your account settings.
Terms of Use: https://example.com/terms
Privacy Policy: https://example.com/privacy
```
## tr · Türkçe
### subtitle
```
Listeler ve notlar, eşitli
```
### promotionalText
```
Yeni sürüm göndermeden istediğin zaman değiştirebileceğin bir iki cümle.
```
### keywords
```
yapılacaklar,kontrol,market,planlayıcı,not,paylaşım,aile,düzen,görev
```
### description
```
İlk paragraf: uygulamanın gördüğü iş ve onunla neden daha kolay olduğu.
ÖZELLİKLER
• Tüm cihazlarında eşitlenen listeler ve notlar
• Bir listeyi ailen ya da arkadaşlarınla paylaş
• Verilerini dışa aktar ya da hesabını Ayarlar'dan istediğin an sil
3 listeye kadar ücretsiz. Example Pro sınırsız liste, paylaşım ve tema açar: 1 hafta ücretsiz denemeli yıllık ya da aylık. Ödeme mağaza hesabınızdan alınır; abonelik, dönem bitmeden en az 24 saat önce iptal edilmezse otomatik yenilenir; hesap ayarlarınızdan yönetebilir ya da iptal edebilirsiniz.
Kullanım Koşulları: https://example.com/terms
Gizlilik Politikası: https://example.com/privacy
```

View file

@ -0,0 +1,69 @@
# Example App · store declarations (answers to copy into the consoles)
Starting answers for a typical **subscription app**: auto-renewable subscriptions (free trial on the yearly plan), an account (email + password, or sign in
with a provider), an analytics SDK acting as your processor, data synced to your own backend, no ads, no chat with
strangers, general audience. **Edit every line to match your app**: these are your statements to the stores. Everything
marked **UI** has no API: Play Console > your app > Policy and programs > **App content**, and App Store Connect for
Apple. Data safety (Play) has an API: `storebound play data-safety`.
## Google Play · App content (UI)
| declaration | answer |
|---|---|
| Privacy policy | your policy URL (same one in the listing and inside the app) |
| App access | "All or some functionality is restricted": add the demo account (username, password) and the steps from `reviewAccess.instructions`; credentials must work at all times, no 2FA / OTP (review-access skill) |
| Ads | No, my app does not contain ads |
| Content rating | IARC questionnaire, below |
| Target audience and content | 18 and over (or 13+ if teens use it); "Could your app unintentionally appeal to children?" No (only if true) |
| News app | No |
| Data safety | `storebound play data-safety`, then check the summary; with accounts: the delete-account URL (`accounts.deletionUrl`) |
| Government apps | No |
| Financial features | My app doesn't provide any financial features |
| Health apps | No health features |
| Advertising ID | No (unless an SDK in your app reads it: check the SDK docs and your merged manifest) |
| Store settings | App, category (Productivity, Tools, ...); contact email + website |
## Account deletion (account-deletion skill)
- In the app: Settings > Account > Delete account deletes the account and its data (not only "deactivate").
- On the web: `accounts.deletionUrl` lets people request deletion without the app; Play asks for it in Data safety.
- With subscriptions: say that deleting the account does not cancel the store subscription, and how to cancel.
## IARC content rating (Play UI; Apple has its own form, `storebound apple appinfo`)
Category: not a game (e.g. Utility, Productivity, Communication, or Social Networking if people share content with
others). Typical answers for a lists / notes app:
| topic | answer |
|---|---|
| Violence, sexuality, language, controlled substances | No |
| Gambling (simulated / real money) | No |
| Users interact or exchange content | Yes only if people can share with others outside their own invitations; then also moderation and reporting |
| Shares location with other users | No |
| Digital purchases | Yes (subscriptions) |
| Unrestricted internet / browser | No |
Expected: PEGI 3, ESRB Everyone, IARC 3+. Apple age rating 4+ (all content questions None) unless users share
content publicly.
## App Store (UI-only parts)
- App record: App Store Connect > Apps > + > New App. No API.
- App Privacy: `app-privacy.md`, then **Publish**.
- App Review information: `storebound apple review-details` sends the contact, the demo account (from
`.storebound/credentials.json`, masked in the log) and the notes.
- Export compliance (export-compliance skill): `ITSAppUsesNonExemptEncryption = NO` only if true.
- Guideline 5.1.1: the privacy policy link must also be reachable **inside the app**; 5.1.1(v): account deletion
inside the app.
## Subscriptions (store-subscriptions skill)
- Purchase screen (both stores): plan name, length, the full renewal price per period (most prominent), what is
included, free trial length and what is charged after it, how to cancel, Restore Purchases, links to the Terms of Use
and the Privacy Policy. Apple 3.1.2; Google Play subscriptions policy.
- Metadata: the App Store description (or a custom EULA in App Information > License Agreement) links the Terms of
Use; the Privacy Policy URL field is set by `storebound apple listing`.
- First subscriptions are submitted together with an app version (App Store Connect: add them to the version on its
page before Submit for Review), like first in-app purchases.
- App Review notes say where the paywall is and that sandbox purchases work with the demo account.
- Play: base plans and offers must be active before people can buy; new subscribers see the converted regional prices.

View file

@ -0,0 +1,67 @@
# Example App · Google Play listing copy
Source for `storebound play listing` (title, short + full description) and `storebound play release` (release notes).
Rules the parser follows:
- every `## ...` section with a `<!-- play: <codes> -->` line is a listing; one section can serve several Play codes
- under it, `### ...` headings that contain **title** (optional, defaults to `app.name`), **short**, **full** and
**what's new**, each followed by one fenced block
- limits (checked before anything is sent): title 30, short 80, full 4000, what's new 500 characters
Only claim what the app really does. No prices, no "best / #1", no other apps' names.
---
## English (en-US)
<!-- play: en-US -->
### Play short description (max 80)
```
What the app does for people, in one plain line
```
### Play full description (max 4000)
```
First paragraph: the job the app does and why it is easier with it, in two or three sentences.
FEATURES
• Lists and notes that sync across your devices
• Share a list with family or friends
• Export your data or delete your account in Settings, any time
Free to use with up to 3 lists. Example Pro unlocks unlimited lists, sharing and themes: yearly with a 1-week free trial, or monthly. Payment is charged to your store account; the subscription renews automatically unless cancelled at least 24 hours before the end of the period, and you can manage or cancel it in your account settings.
Terms of Use: https://example.com/terms
Privacy Policy: https://example.com/privacy
```
### What's new (max 500)
```
First release. Thanks for trying it!
```
## Türkçe (tr-TR)
<!-- play: tr-TR -->
### Play kısa açıklama · short (max 80)
```
Uygulamanın insanlar için ne yaptığını anlatan sade bir cümle
```
### Play tam açıklama · full (max 4000)
```
İlk paragraf: uygulamanın gördüğü iş ve onunla neden daha kolay olduğu, iki üç cümlede.
ÖZELLİKLER
• Tüm cihazlarında eşitlenen listeler ve notlar
• Bir listeyi ailen ya da arkadaşlarınla paylaş
• Verilerini dışa aktar ya da hesabını Ayarlar'dan istediğin an sil
3 listeye kadar ücretsiz. Example Pro sınırsız liste, paylaşım ve tema açar: 1 hafta ücretsiz denemeli yıllık ya da aylık. Ödeme mağaza hesabınızdan alınır; abonelik, dönem bitmeden en az 24 saat önce iptal edilmezse otomatik yenilenir; hesap ayarlarınızdan yönetebilir ya da iptal edebilirsiniz.
Kullanım Koşulları: https://example.com/terms
Gizlilik Politikası: https://example.com/privacy
```
### Yenilikler · what's new (max 500)
```
İlk sürüm. Denediğin için teşekkürler!
```

View file

@ -0,0 +1,159 @@
// storebound config for one app (profile "subscription-app": accounts plus auto-renewable subscriptions, an
// analytics SDK, no ads). Every answer below is a starting point: edit it to match your app. Credentials NEVER go
// here (env vars or .storebound/credentials.json, gitignored). Paths are relative to this file.
// The type import is optional: it only works when storebound is installed in the project (bun add -d storebound).
import type { ShipConfig } from 'storebound';
const config: ShipConfig = {
app: {
name: 'Example App',
profile: 'subscription-app',
copyright: '2026 Example Studio',
privacyPolicyUrl: 'https://example.com/privacy',
termsUrl: 'https://example.com/terms', // Terms of Use (EULA): in the app and the metadata (Apple 3.1.2)
supportUrl: 'https://example.com/support',
marketingUrl: 'https://example.com',
website: 'https://example.com',
contactEmail: 'support@example.com',
},
// Accounts (account-deletion skill). Remove this block if the app has no sign-up / sign-in at all.
// Apple 5.1.1(v): deletion must start inside the app. Google Play: in the app AND on a public web page.
accounts: {
enabled: true,
deletionUrl: 'https://example.com/delete-account', // public page, no login needed to request deletion
inApp: 'Settings > Account > Delete account', // goes into the App Review notes
},
// Sign-in for store reviewers (review-access skill). The demo account itself lives ONLY in
// .storebound/credentials.json ("reviewDemoAccount") or REVIEW_DEMO_USER / REVIEW_DEMO_PASSWORD.
reviewAccess: {
signInRequired: true, // false if every feature works without signing in
instructions: 'Sign in with the demo account. Sample data is already there; nothing needs a second device.',
},
// id = your folder / copy key; play = Play language code(s); apple = App Store Connect locale
languages: [
{ id: 'en', play: 'en-US', apple: 'en-US' },
{ id: 'tr', play: 'tr-TR', apple: 'tr' },
],
play: {
packageName: 'com.example.app',
defaultLanguage: 'en-US',
listing: 'store/listing.md',
icon: 'store/icons/play-512.png',
},
apple: {
bundleId: 'com.example.app',
listing: 'store/appstore.md',
// Export compliance (export-compliance skill): decide it, then uncomment. false ONLY if the app uses no encryption
// beyond the OS (HTTPS through the system, standard sign-in).
// usesNonExemptEncryption: false,
// ipad: true,
info: {
categories: { primary: 'PRODUCTIVITY', secondary: 'UTILITIES' },
ageRating: {}, // every content question NONE / false; change what applies (user-generated content, web access ...)
contentRights: 'DOES_NOT_USE_THIRD_PARTY_CONTENT',
free: true,
reviewNotes: [
'Example App keeps lists and notes in sync across devices. Pro (yearly with a 1-week free trial, or monthly) unlocks unlimited lists, sharing and themes.',
'Subscriptions: Settings > Pro (or the paywall after the third list). Restore Purchases is on the paywall and in Settings > Pro.',
'Privacy policy and Terms of Use: on the paywall and in Settings > About.',
].join('\n\n'),
},
},
// Auto-renewable subscriptions (store-subscriptions skill): `storebound apple subscriptions`, `storebound play
// subscriptions`. Apple: one group, levels rank the plans (1 = highest). Play: one subscription per entry with one
// auto-renewing base plan. Prices: USD base, Apple equalizes from the USA, Play converts per region; overrides set a
// region by hand. The disclosure (price per period, renewal, how to cancel, Terms + Privacy links) belongs on the
// purchase screen and in the descriptions.
subscriptions: [
{
name: 'Example Pro',
listings: { en: { name: 'Example Pro' }, tr: { name: 'Example Pro' } },
subscriptions: [
{
id: 'com.example.app.pro_yearly', period: 'P1Y', level: 1, usd: 29.99,
listings: {
en: { title: 'Pro yearly', description: 'Unlimited lists, sharing and themes.', benefits: ['Unlimited lists', 'Shared lists', 'All themes'] },
tr: { title: 'Pro yıllık', description: 'Sınırsız liste, paylaşım ve tema.', benefits: ['Sınırsız liste', 'Paylaşılan listeler', 'Tüm temalar'] },
},
intro: { type: 'free', duration: 'P1W' }, // 1 week free trial for new subscribers
reviewScreenshot: 'store/iap-review/paywall.png',
},
{
id: 'com.example.app.pro_monthly', period: 'P1M', level: 2, usd: 3.99,
overrides: [{ region: 'TR', currency: 'TRY', price: 99.99 }],
listings: {
en: { title: 'Pro monthly', description: 'Unlimited lists, sharing and themes.', benefits: ['Unlimited lists', 'Shared lists', 'All themes'] },
tr: { title: 'Pro aylık', description: 'Sınırsız liste, paylaşım ve tema.', benefits: ['Sınırsız liste', 'Paylaşılan listeler', 'Tüm temalar'] },
},
reviewScreenshot: 'store/iap-review/paywall.png',
},
],
},
],
// Google Play Data safety. Types not listed = not collected. Typical app with accounts + an analytics SDK acting as
// your processor (collected, not shared). Edit to match what your app and its SDKs really do.
dataSafety: {
collectsData: true,
encryptedInTransit: true,
deletionRequest: true,
// Sign-up methods. Google does not publish these ids: compare with Play Console > App content > Data safety >
// Export to CSV before the first import. The delete-account URL question that follows: add it under `extra` with
// the exact id from that export, e.g. { question: '<id from the export>', value: 'https://example.com/delete-account' }.
accountCreation: ['PSL_ACM_USER_ID_PASSWORD'],
data: {
PSL_EMAIL: { shared: false, collect: ['functionality', 'account'] },
PSL_USER_ACCOUNT: { shared: false, collect: ['functionality', 'account'] },
PSL_USER_GENERATED_CONTENT: { shared: false, collect: ['functionality'] },
PSL_USER_INTERACTION: { shared: false, collect: ['analytics'] },
PSL_CRASH_LOGS: { shared: false, collect: ['analytics'] },
PSL_PERFORMANCE_DIAGNOSTICS: { shared: false, collect: ['analytics'] },
PSL_DEVICE_ID: { shared: false, collect: ['analytics'] },
PSL_PURCHASE_HISTORY: { shared: false, collect: ['functionality'] }, // subscription status tied to the account
},
},
renders: 'store/renders',
shots: {
style: 'app', // calm gradient, device frame, headline + subline (game: sparkles and badges)
copy: 'store/shots/copy.json',
frames: 'store/shots/frames', // <lang>/phone/<name>.png, <lang>/tablet/<name>.png
out: 'store/renders',
// fonts: { title: 'Inter', body: 'Inter' }, // the app style default
scenes: [
{ id: 'home', palette: 'mist', frames: [{ src: 'home.png' }] },
{ id: 'detail', palette: 'mint', frames: [{ src: 'detail.png' }] },
{ id: 'share', palette: 'sky', frames: [{ src: 'share.png' }] },
{ id: 'privacy', palette: 'slate', frames: [{ src: 'settings.png' }] },
],
feature: { frame: 'home.png', palette: 'mist' },
// Raw frames from a web build, the iOS Simulator (ios-sim) or an Android emulator (android-emu): sign in to a demo
// account with realistic sample data, never real user data (store-screenshots skill).
capture: {
baseUrl: 'http://127.0.0.1:5173',
query: 'lang={lang}&demo=1',
shots: {
'home': { path: '/', delayMs: 1000, device: 'both' },
'detail': { path: '/lists/1', delayMs: 1000, device: 'both' },
'share': { path: '/lists/1/share', delayMs: 1000, device: 'both' },
'settings': { path: '/settings', delayMs: 1000, device: 'both' },
},
},
},
icons: {
composite: 'store/icon/icon.png',
foreground: 'store/icon/foreground.png',
background: 'store/icon/background.png',
out: 'store/icons',
},
};
export default config;

View file

@ -6,6 +6,7 @@ import type { ShipConfig } from 'storebound';
const config: ShipConfig = { const config: ShipConfig = {
app: { app: {
name: 'Example Game', name: 'Example Game',
profile: 'game',
copyright: '2026 Example Studio', copyright: '2026 Example Studio',
privacyPolicyUrl: 'https://example.com/privacy', privacyPolicyUrl: 'https://example.com/privacy',
supportUrl: 'https://example.com', supportUrl: 'https://example.com',

View file

@ -12,5 +12,5 @@
"skipLibCheck": true, "skipLibCheck": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"include": ["cli", "tests", "tools", "templates/storebound.config.ts", "examples/demo-game/storebound.config.ts", "examples/demo-game/serve.ts", "media/teaser/render.ts"] "include": ["cli", "tests", "tools", "templates/storebound.config.ts", "examples/demo-game/storebound.config.ts", "examples/demo-game/serve.ts", "examples/demo-app/storebound.config.ts", "examples/demo-app/serve.ts", "templates/profiles/*/storebound.config.ts", "media/teaser/render.ts"]
} }