In-app purchases.
Store products and server verification.
Users buy plans, credit packs and Remove ads through Google Play or the App Store. Nothing is granted until your backend has verified the purchase with the store, and one purchase entitles one account at a time. This page covers the products, the store keys the backend needs, and what happens on a purchase.
01The store products
A product is an item you create in each store with a product id and a price. The app sells the ids you enter in the console; prices are set in the stores, never in the code.
You must create your own products. The product ids you see in the console (pro_monthly, pro_yearly, remove_ads, credits_small, credits_medium, credits_large) are placeholder names: no such product exists in any store account until you create it in your own Google Play Console and App Store Connect, and the app sells nothing until then. No product id, store key or store account of the author ships, and the backend verifies purchases only with the credentials you enter (check it yourself).
| Example id (a name, not a product) | Console | Google Play type | App Store type | Effect after verification |
|---|---|---|---|---|
pro_monthly, pro_yearly (and any weekly ids you add) | Plans → the plan → products per period | Subscription (one per period) | Auto-renewable subscription | The plan until the period ends; renewals extend it |
A lifetime id you choose (for example pro_lifetime) | Plans → lifetime | One-time product | Non-consumable | The plan for life |
remove_ads | Store & purchases → Remove-ads product id | One-time product | Non-consumable | No ads |
credits_small, credits_medium, credits_large | Store & purchases → Credit pack S / M / L product id | One-time product (bought as consumable) | Consumable | + credits in the pack (default 50, 200, 600) |
These ids are example names. No product with these names exists until you create it in your own Google Play Console and App Store Connect; you can also type the ids you already have. The console says so under each field. The example plan sells only monthly and yearly; weekly and lifetime are empty until you add ids. Click paths for creating the products: Google Play Console, App Store Connect. A free trial, if you want one, is an introductory offer on the store product; the plan's trial days only change the text in the app.
02Everything is configured by you
Purchases are entirely buyer-configured. Nothing of the author is in the code:
- Product ids: the defaults above (
pro_monthly,remove_ads,credits_small, …) are plain placeholder names, not products of any account. A product id only means something inside the store account that owns the app's package name or bundle id, which is yours after you rename the app (Release → ids). You can keep these names or enter your own in the console. - Verification endpoints: the backend talks only to the stores' official APIs,
androidpublisher.googleapis.com(Google Play Developer API) andapi.storekit.itunes.apple.com(App Store Server API;api.storekit-sandbox.itunes.apple.comin sandbox), authenticated with your service account and your App Store key from Console → Store & purchases (firebase/functions/src/iap.ts). No receipt is sent anywhere else. - The app's backend address is your own
API_BASE_URL, set when you build; it is empty in the source (app/lib/core/env.dart), and the app shows a setup screen without it. - Credentials: the package name, service account JSON, issuer id, key id,
.p8key and bundle id are empty until you enter them; the secret ones are stored encrypted and written only toconfig/private(schema). No Google service account key and no App Store key is in the source: check it yourself. - Money goes from Google Play and the App Store to your developer accounts. The code pays and reports nothing to the author.
03No store key ships: check it yourself
In plain words: the two keys that let a server ask Google Play and the App Store about purchases are yours. They do not exist in the download. You paste them into the console after you install it, and they never leave your server.
| Key | In the download | Where yours goes | Where it is stored |
|---|---|---|---|
| Google Play service account (JSON key file) | Not included. No .json key file, no private key block. | Console → Store & purchases → Google Play service account (JSON), or none at all if you use the Cloud Functions runtime account | Encrypted in the console's storage; written only to config/private.iap.googleServiceAccountJson |
App Store In-App Purchase key (.p8) | Not included. No .p8 file, no private key block. | Console → Store & purchases → In-app purchase key (.p8 contents) | Encrypted in the console's storage; written only to config/private.iap.applePrivateKey |
| Package name, bundle id, issuer id, key id | Empty strings | The fields next to the keys | config/private.iap |
Three ways to verify it in the source:
- The list of secret fields
admin/src/sync.ts,SECRET_KEYS, names every secret the console can hold, includingiap.googleServiceAccountJsonandiap.applePrivateKey. They are write-only fields without a default (admin/src/manifest.ts, sectionstore), and the backend's defaults for them are empty strings (firebase/functions/src/api/defaults.ts,DEFAULT_PRIVATE.iap). - One commandFrom
admin/runtool/check_no_keys.sh. It searches the whole source folder for key files (.p8,.p12,.pem, keystores, service account files,google-services.json,GoogleService-Info.plist,.env), private key blocks and literal API keys, prints file names only, and ends withPASS no key or credential file is in …. - The tests
admin/test/manifest.test.ts("no ad id, store key or store identifier ships as a default") andfirebase/functions/test/unit/defaults.test.tsfail if a default stops being empty.
Terminal: macOS or Linux as shown; on Windows use WSL or Git Bash (how).
After you have set up the project, the same command fails as soon as a .env, a service account file or a key file lies inside the source folder. Run it before you send your copy to a developer or put it in a repository.
04Google Play verification
- Package nameConsole → Store & purchases → Android package name: your application id (for example
com.yourcompany.yourapp). - Access to the Play Developer APIEnable "Google Play Android Developer API" in Google Cloud, then invite the service account in Play Console → Users and permissions with View financial data for your app. By default the backend uses the Cloud Functions runtime service account (
<project-number>-compute@developer.gserviceaccount.com). - Or a separate service accountPaste its whole JSON key into Google Play service account (JSON) instead, and invite that account's e-mail the same way. The console's test only checks the file's shape ("Service account … — the file is complete."); nothing is sent.
Every click: Provider dashboards → Google Play Console.
05App Store verification
- Create an In-App Purchase keyApp Store Connect → Users and Access → Integrations → In-App Purchase → generate a key and download the
.p8file (it downloads once). - Enter four valuesConsole → Store & purchases: iOS bundle id, App Store Connect issuer id, In-app purchase key id and In-app purchase key (.p8 contents) (paste the whole file including the BEGIN and END lines). The test checks that the key parses as an EC key.
- EnvironmentApp Store environment: Sandbox (TestFlight) while you test, Production for the live app.
The backend reads each transaction from the App Store Server API with a token signed by this key. Every click: Provider dashboards → App Store Connect.
06Switch purchases on
- Products approvedCreate and activate the products in both stores (Apple needs them submitted with an app version the first time).
- PlansConsole → Plans: product ids, features, limits, Active (Plans and paywall).
- Master switchConsole → Store & purchases → Sell in-app purchases on, Save.
- Check Status"Store purchases configured" and "Plans are sellable" should pass.
- TestAndroid: upload a build to an internal testing track and add licence testers in Play Console. iOS: TestFlight with a sandbox account and the Sandbox environment.
07What happens on a purchase
- The store chargesThe app buys through Google Play Billing or StoreKit and receives a purchase token (Android) or transaction id (iOS).
- The app asks the backend
POST /v1/purchases/verifywith the product id and the token. - The backend verifieswith Google or Apple, maps the product id to a plan and period (or to Remove ads or a credit pack), then grants it once per order, stores
purchases/{platform}_{orderId}and, when the store reports a price, writes one revenue event for the Revenue centre.
08One purchase, one account at a time
- A plan or Remove ads bought once belongs to one account at a time. If the same store purchase is restored while signed in to another account, the entitlement moves to that account and is removed from the previous one.
- A credit pack is credited once. Restoring or re-sending the same order never adds credits again.
- A plan granted by hand in the console (Users) is separate from the store plan and is never touched by a store purchase or restore; the user gets the active plan with the higher rank.
- For a plan to survive a reinstall or a new phone, the user should sign in (Google, Apple or e-mail) and tap Restore purchases.
09Before verification is set up
Until the package name (Android) or the four Apple values (iOS) are set, verification answers 503 {"error":"iap_unconfigured"} and grants nothing. The app then leaves the store transaction pending instead of completing it: the store delivers it again on the next start and the app retries, and Google Play refunds a purchase that stays unacknowledged for 3 days. Still, switch purchases on only after verification is set up, so no buyer waits. A purchase the backend rejects for good (an invalid token) is completed and not granted. Fixes: Troubleshooting → purchases.
10Records and account deletion
When a user deletes their account, purchases and revenueEvents are kept (with the user id as an opaque id) because you may have to keep financial records. Mention this in your privacy policy (Account deletion).