storebound/skills/store-declarations/SKILL.md

13 KiB

name description
store-declarations Answer the store compliance forms for a mobile app or game - Google Play Data safety (API CSV), App content declarations (ads, app access, content rating IARC, target audience, advertising ID, government/financial/health/news), App Store App Privacy nutrition labels, age rating, export compliance. Use when the user fills in data safety, privacy labels, content rating, target audience, ads or advertising ID declarations, or gets Data safety CSV import errors.

Store declarations

Do it, don't ask (first rule in AGENTS.md): you fill these forms yourself (API, or the console with your browser tool from storebound console plan). Ask the owner only the answers no code or config proves, all in one batch at the start, and save them in the config; the owner presses only App Privacy Publish, Play's "Send changes for review" (Publishing overview) and a console form's final Submit / Create, and ticks the IARC Terms of Use box before you start. Saving a Play App content card is yours (it is not public before Send changes for review).

storebound scan proposes the Data safety and App Privacy answers from the detected SDKs and code (each item "detected (file:line)" or "ask owner") as a diff against dataSafety / appPrivacy; --write adds only the detected ones after the owner agrees.

Templates: store/declarations.md (Play App content + IARC answers), store/app-privacy.md (Apple labels), both from storebound init --profile app|game|subscription-app (default app). They are honest starting points for a typical app of that kind, not your answers: edit every line to match what your app really does. Data safety answers live in dataSafety of storebound.config.ts. CLI: storebound (or bunx storebound in a project that has it, or bun <toolchain>/cli/index.ts).

Order

  1. Inventory first. List every SDK and server call: what leaves the device, who receives it, can the user turn it off. The privacy policy (privacy-policy skill) must say the same; update it before the forms.
  2. Play Data safety (API). Fill dataSafety (types not listed = not collected; shared = third party that is not your service provider, e.g. ad networks; analytics vendors acting as processors are "collected" only). storebound play data-safety --dry --csv store/data-safety.csv to review, then storebound play data-safety. Play cannot read the form back: storebound keeps the hash of the last upload in .storebound/data-safety.sha256 and skips an unchanged CSV. After a hand edit in Play Console, --force sends the config's answers again. Then check the summary in Play Console > Policy and programs > App content > Data safety.
  3. Play App content (UI, you drive it). storebound console plan play-app-content --json lists every card of /app-content/overview in order (Privacy policy, Ads, Sign in details, Content ratings, Target audience and content, Data safety, Advertising ID, Government apps, Financial features, Health apps) with its page, start button (aria-label "Start declaration") and its own plan, plus ONE owner batch (ownerBatch, printed first) of what the config does not prove and what the owner does. Hand it over at the start and write the answers into the config:
    • console.contentRating.answers and .details (the IARC follow-ups each Yes reveals: violence details, other for fear / sexuality / gambling / language / controlled substances / crude humor, player trading, the user interaction follow-ups, the Miscellaneous questions: Nazi symbols, Korea history, terrorism, crimes);
    • products[].randomRewards (loot boxes: proven per product; ClassInd rated such a game 18), social (chat, block, report, moderation, friends only, location sharing);
    • console.appContent (government, financialFeatures, healthFeatures, advertisingId), reviewAccess. The IARC Terms of Use box is an agreement: the owner ticks it on the questionnaire's page 1 BEFORE your run (it is in the owner batch; page 1's Next does nothing until it is ticked, verified 2026-10-13). Each card's Save and the IARC Summary's Save (not "Submit") are yours: a saved declaration goes to Google only with the next "Send changes for review" in Publishing overview, the owner's one button at the end. console plan play-app-content --all walks every card in order and ends with that owner list. The Sign in details demo account: type it from .storebound/credentials.json (reviewDemoAccount) without printing it; the owner types it only when it is kept out of there.
  4. Apple age rating (API). storebound apple appinfo (all NONE except your overrides, e.g. advertising: true).
  5. Apple App Privacy (UI only, you drive it: storebound console plan apple-app-privacy). App Store Connect > the app > App Privacy > Get Started / Edit, from store/app-privacy.md; per data type: purposes > linked to identity > used for tracking, Save. Then the owner presses Publish.
    • Purposes follow Apple's definitions (developer.apple.com/app-store/app-privacy-details/), one per Play purpose: Play advertising (ads from an ad SDK) is Third-Party Advertising only, never also Developer's Advertising or Marketing (that one is first-party ads or marketing to your own users: the owner's answer); analytics is Analytics (not Product Personalization); personalization is Product Personalization; functionality, fraud, account and communications are App Functionality (marketing messages: ask the owner).
    • The owner's answers go into appPrivacy.types, keyed by Apple's type id (USER_ID, DEVICE_ID, GAMEPLAY_CONTENT, ADVERTISING_DATA, CUSTOMER_SUPPORT, ENVIRONMENT_SCANNING, HANDS, HEAD ... all 35 in cli/lib/app-privacy.ts):
      appPrivacy: { tracking: true, types: {
        DEVICE_ID: { collected: true, linked: false, tracking: true, purposes: ['THIRD_PARTY_ADVERTISING', 'ANALYTICS'] },
        ADVERTISING_DATA: { collected: true, linked: false, tracking: true, purposes: ['THIRD_PARTY_ADVERTISING'] },
        GAMEPLAY_CONTENT: { collected: false },
      } },
      
      Purposes: THIRD_PARTY_ADVERTISING, DEVELOPERS_ADVERTISING, ANALYTICS, PRODUCT_PERSONALIZATION, APP_FUNCTIONALITY, OTHER_PURPOSES. A set field wins over what storebound derives from dataSafety; types with no Play equivalent (Advertising Data, Gameplay Content, Customer Support, Environment Scanning, Hands, Head) are answered only here. scan --write proposes collected + purposes for detected types the block does not answer yet; linked and tracking are never proposed (ask the owner). storebound check compares the block with the iOS project: tracking true <-> NSUserTrackingUsageDescription and a manifest that declares tracking: the tracking SDK's own manifest (Pods, frameworks, plugins, the built IPA; info) or the app's, which says true only with its own NSPrivacyTrackingDomains (true with an empty list is ITMS-91064, see privacy-manifest); the app's NSPrivacyCollectedDataTypes <-> the types marked collected (mismatches both ways).
  6. Export compliance. export-compliance skill (ITSAppUsesNonExemptEncryption in Info.plist, apple encryption).
  7. Privacy manifest (iOS). privacy-manifest skill; keep it consistent with App Privacy.

Typical answers per profile (starting points, edit to match your app)

  • app (utility / productivity, optional account, analytics, no ads): Data safety: email + user ids (account, collected, functionality + account management), app interactions and diagnostics (analytics SDK as your processor: collected, not shared), crash logs. Encrypted in transit: yes. Deletion: yes (in the app and on the web, see account-deletion). Account creation: the methods you offer (e.g. PSL_ACM_USER_ID_PASSWORD, PSL_ACM_OAUTH). Ads: No. Advertising ID: No (unless an SDK reads it). App access: sign-in needed, demo account given (review-access). IARC category: not a game (utility, productivity, communication or social, reference ...). Apple: Contact Info > Email Address and Identifiers > User ID are linked to the user (account), not used for tracking.
  • subscription-app: as app, plus Purchases > Purchase History (linked, App Functionality) and Play PSL_PURCHASE_HISTORY (collected, functionality). IARC: digital purchases Yes. App Review: subscriptions and their disclosure (store-subscriptions).
  • game (single-player free-to-play with ads + IAP, no accounts): approximate location (IP country; shared, analytics + ads + fraud), app interactions (shared, analytics + ads), other app activity (save backup, functionality), diagnostics (shared), device or other IDs (shared, ad id), purchase history (if your server checks receipts). Encrypted in transit: yes. Deletion: yes, by email. Account creation: none. IARC (for games): everything No except digital purchases Yes; target audience 13+; ads Yes; advertising ID yes (advertising, analytics, fraud prevention); sign-in details: no login needed.

Gotchas

  • The Data safety CSV import replaces the whole form.
  • PSL_SUPPORTED_ACCOUNT_CREATION_METHODS (e.g. PSL_ACM_NONE) is required even though Google's older sample CSV omits it: without it the import fails with "Response missing". storebound always writes it.
  • Conditional required rows: some questions become required by another answer, and Play refuses the import with "Response missing for " (400). Account creation not PSL_ACM_NONE -> PSL_ACCOUNT_DELETION_URL (from accounts.deletionUrl) and PSL_SUPPORT_DATA_DELETION_BY_USER, the GENERAL "Do you provide a way for users to request that their data is deleted?" (single choice DATA_DELETION_YES / DATA_DELETION_NO / DATA_DELETION_NO_AUTO_DELETED). A delete-account page is such a way, so with accounts.deletionUrl storebound answers yes, and check flags anything else next to a delete-account URL. Without one: dataSafety.dataDeletion: 'yes' | 'no' | 'auto90'. A yes needs PSL_DATA_DELETION_URL ("Delete data URL"): dataSafety.partialDeletionUrl for a page that deletes data without deleting the account (partialDeletion: true), else the delete-account page; PSL_HAS_OUTSIDE_APP_ACCOUNTS is refused next to sign-up methods. A selected data type needs collected / shared, ephemeral, required / optional and a purpose (a sharing purpose when shared). storebound check and play data-safety --dry name each missing row as missing: <id> (<when>; set <config field>) and the real run stops before sending. Rules: DATA_SAFETY_RULES / typeRules in cli/lib/data-safety.ts. A dataSafety.extra row with a generated question id replaces that question (use it when your Play Console export shows other ids).
  • Only the current form's ids: Play answers 400 ("Line N: Response ID missing", "Invalid response ID") for a row its form does not have, e.g. a choice question written as a bare TRUE / FALSE, or a removed question (PSL_DATA_COLLECTION_USER_REQUEST_DELETE is gone, so dataSafety.deletionRequest is no longer used). storebound checks every (Question ID, Response ID) pair, dataSafety.extra included, against Google's empty form in cli/data/play-data-safety-form.csv (exported 2026-10-12) and takes each row's Answer requirement from it. When Play changes the form: Play Console > App content > Data safety > Export to CSV with nothing answered, save it over that file, run the tests.
  • "Optional" data is only allowed when the user can really choose for that data; otherwise required.
  • Ads SDKs: read each network's own Play data safety guidance; adding a network can add data types.
  • Target audience under 13 pulls the app into the Families policy (certified ad networks, no ad id): only if true.
  • Apple: "linked to the user" is No when there are no accounts and nothing ties data to a name / email / phone; with accounts, what you store under the account is linked.
  • Accounts: Play asks for a delete-account URL in Data safety and Apple wants deletion inside the app (account-deletion skill).
  • Accounts are not email. A guest / username account (a name picked on the device, a server token, no password, no email) is User ID only (Play PSL_USER_ACCOUNT, Apple Identifiers > User ID) and account creation "Username and other authentication" (PSL_ACM_USER_ID_OTHER_AUTH): set accounts.signUp: ['username']. storebound scan marks Email "detected" only for an email field, an email property in a request or schema, an email validation or endpoint, an email sign-in call, an SDK that collects it (Google / Apple / Firebase sign-in) or signUp: ['email-…']; built bundles (dist, build, the Capacitor webDir, minified files) only make it a question.
  • App Privacy saved but not published = not submitted.
  • Keep store answers, the privacy policy and the in-app privacy settings consistent; reviewers compare them.