Keeping your privacy policy in sync with your build
A privacy policy is written once and goes stale the day a pull request adds an SDK. How to notice it on the pull request itself, with a free check that reads only your build files.
Record which SDKs, data types and permissions your pages were written from, then compare every pull request's build files with that record. When a dependency or permission appears that the record does not have, the pull request shows it before the release does — while the privacy policy and store forms are still easy to update.
Do this in a few minutes instead
AppFoyer hosts the pages this guide is about — privacy policy, terms, support, account
deletion and app-ads.txt — on their own
subdomain, with no domain or server of your own. One app is free, and nothing a store
requires is ever behind a paywall.
Checked 30 September 2026. Store policies change, sometimes without notice. Everything below links to the official documentation, and that page — not this one — is the authority. If the two disagree, the store is right.
Writing a privacy policy is a one-time job. Keeping it true is not: the policy describes the build you had when you wrote it, and the build keeps changing. A pull request adds crash reporting, a subscription library or a location permission, the app ships, and the policy, the Data safety form and the App Privacy details still describe the old app.
Both stores treat that as your problem, not theirs. Google Play says your Data safety answers must stay accurate and complete at all times, and that data collected by third-party libraries and SDKs in your app is yours to declare. Apple says the same about App Privacy details, and adds that you can change the answers in App Store Connect at any time without submitting an update.
Where the drift actually comes from
Almost never from a decision to “collect more data”. It comes from ordinary engineering:
- A new dependency. Crash reporting, analytics, a paywall SDK, a maps library.
- A new permission. Location for one feature, the camera for a scanner, tracking for ads.
- A flavor or target. A free build gains an ad SDK the paid build does not have.
- A removal. Also drift — the policy now over-declares, which is the safer mistake but still a mistake.
Every one of these lands in a build file — build.gradle, AndroidManifest.xml, Podfile,
Package.swift, Info.plist, pubspec.yaml, package.json. That is what makes it checkable.
The approach: a record, and a check on every pull request
- When your pages are written, record what they were written from: the SDKs, the data types and the permissions in the build at that moment.
- On every pull request, read the build files again and compare.
- When something differs, say what, where (file and line), and which page or form it may affect.
Two properties matter. The check must read build files only — never source code, never secrets — so it is safe to run on any repository. And it must say “may be outdated”, not “violation”: a library in a build file is not proof the app uses it, and only you know whether it changes what your app collects.
Setting it up with AppFoyer
The check is free on every plan. It reads build files only, needs no key or secret, sends nothing anywhere and never changes a page.
With the Claude Code plugin
- Say “make this app store-ready”. Besides drafting the pages, the plugin writes
appfoyer.jsonat the root of your repository: the record of what the pages were written from. - Say “add the AppFoyer drift check”. It shows you a GitHub workflow and writes
.github/workflows/appfoyer.ymlwhen you say yes. - Commit both files. The plugin never commits or pushes for you.
Without the plugin
Add the workflow yourself:
name: AppFoyer drift check
on: pull_request
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: appfoyer/check-action@v1
Open a pull request. With no appfoyer.json in the repository yet, the job summary shows the file
it would write from your build. Compare it with your published pages and commit it.
Or create it locally — the same scanner, published on npm:
npx @appfoyer/check init
On another CI system
The npm package runs anywhere Node 20 or later does. For GitLab, for example:
appfoyer-drift-check:
image: node:22
script:
- npx -y @appfoyer/check --fail-on-drift
--format markdown gives the same report the GitHub job summary shows; --format json is for
scripts.
What a finding looks like
A pull request that adds RevenueCat to an app whose pages were written without it:
"Trail Notes": 2 changes
+ In-app purchases / subscriptions (data collected) app/build.gradle.kts:28
→ Privacy policy may not declare "In-app purchases / subscriptions"
→ Google Play Data safety and Apple App Privacy answers may be outdated
→ Terms may not cover purchases and subscriptions
+ RevenueCat (SDK) app/build.gradle.kts:28
→ Store forms: RevenueCat is not in the app's SDK list
→ Privacy policy may not name this SDK
By default the job reports and passes. Add fail-on-drift: "true" under with: to make it fail
until the record is updated.
When it reports a change
- Update what the change affects. Say “make this app store-ready” again on the same branch:
the plugin reads the build again, updates the app’s facts, redrafts the pages the change touches
and rewrites
appfoyer.json. Or edit the pages and the Store forms tab in the dashboard yourself. - Refresh the record. The plugin already did it. After a dashboard edit, run
npx @appfoyer/check update, or copy the refreshed file from the job summary, where it sits under the findings. - Commit it in the same pull request. The next run shows no changes, and the history shows the dependency and the disclosure change arriving together.
- Update the store forms in the consoles. Neither the check nor AppFoyer can reach Play Console or App Store Connect. The draft answers are in the dashboard’s Store forms tab.
- Publish the updated drafts from the review screen.
Several apps in one repository
appfoyer.json holds one entry per store listing. A product flavor with its own application id
gets its own entry, so an ad SDK used only by the free flavor is compared with the free flavor’s
pages and not the paid one’s. The plugin writes one entry per flavor it made store-ready. By hand,
an entry takes a sourceSets list — ["free"], or for a multi-dimension build every flavor in the
combination plus the combination itself — and a root folder when the app sits inside a monorepo.
What it cannot see
- Dependencies of dependencies, except on iOS, where
Podfile.locklists them. An SDK pulled in by another library does not appear in your build file, so it does not appear here either. - Code-only configuration.
app.config.js/app.config.tsin Expo is JavaScript and is not read;app.jsonandpackage.jsonare. - Whether you use what you declare. A dependency that is present but unused is still reported.
- Your backend. Nothing in a build file says what your server stores or for how long.
It is a reminder at the moment a change is cheapest to disclose, not a compliance verdict and not legal advice.
The authoritative sources
- Provide information for Google Play’s Data safety section — Google Play Console Help: keeping the form accurate, and third-party SDK data
- App privacy details on the App Store — Apple: keeping responses up to date, and updating them without an app update
appfoyer/check-actionand@appfoyer/check— the Action and the npm package, with what they read