Your own keys and URLs

RPC endpoints, market-data keys, the privacy policy and the account-deletion URL — the four things that must be yours before you submit.

6 min readUpdated 26 August 2026mobile, keys, rpc, privacy-policy, account-deletion

Four things in a published build must be yours. Three of them work in a development build with our placeholders, which is exactly why they get missed.

RPC endpoints

Your deployment talks to chains through RPC providers. Vendor or demo endpoints are shared quota: your customers' traffic competes with everyone else's, and the provider rate-limits the endpoint rather than the app, so the failure looks like your platform being slow.

Register your own with whichever providers you use, restrict them to your own origins, and put them in your server configuration — not in the app. The client never talks to a chain directly.

Give each chain more than one endpoint while you are there. From Core 6.7.2 a chain accepts a comma-separated list, moves to the fastest, drops one that keeps failing and picks it up again when it recovers — which matters more for an app than for a website, because a rate-limited endpoint reaches your customer as a balance that will not load.

An Origin not allowed error from a provider is nearly always the dashboard allow-list rather than a bad key.

Market-data keys

Same argument, plus one of Apple's. The packaged app_config.json ships with empty placeholders for the client-side keys, and package_for_codecanyon.sh overwrites the working file with the example on the way into the archive, so a vendor key cannot reach a buyer by accident.

Put your own in, and prefer proxying quota-bearing data through your backend so the key is not in a binary at all. Apple 5.2.2 treats shipping a third party's credentials in your app as your problem, not theirs.

A Firebase project, if you want push notifications

Push is optional and off until you configure it. The app builds, runs and publishes with no Firebase project at all — it simply never asks for notification permission and never receives anything.

It has to work that way. The app ships as source, so a Firebase configuration committed to it would be ours, registering your customers' devices against our project. And the Google Services Gradle plugin fails the build outright when its configuration file is missing, so requiring it would mean your first flutter build failing on a file you had never heard of.

To turn it on:

  1. Create a Firebase project and add an Android app using this module's application id.

  2. Download google-services.json into android/app/. The build detects it and switches push on; you will see google-services.json found - Firebase push ENABLED in the Gradle output. Without it you get the same line saying DISABLED, and the build succeeds.

  3. For iOS, add GoogleService-Info.plist to the Runner target and upload an APNs key in the Firebase console. iOS delivers through APNs whatever Firebase is doing, so an app with FCM configured and no APNs key sends nothing and reports no error.

  4. Configure the server halfFCM_PROJECT_ID, FCM_CLIENT_EMAIL and FCM_PRIVATE_KEY from a service account in the same project. See Notifications. Neither half reports the other's absence.

android/app/google-services.json and ios/Runner/GoogleService-Info.plist identify your project, and package_for_codecanyon.sh copies android/ and ios/ wholesale. Both are gitignored for the same reason android/key.properties is. A buyer creates their own; there is nothing to hand them.

A privacy policy URL

Required by both stores, on a URL you control, describing your service — what you collect, why, how long you keep it, and who you share it with. A policy that names the vendor rather than the operator is a rejection.

An account-deletion URL

Both stores require an in-app path to delete an account and a public URL that explains it. The platform ships both: deletion anonymises the record inside the same transaction as the soft delete — email tombstoned, password, phone, avatar, username, wallet address, profile and settings cleared — and the email address is released, so a deleted customer can register again.

The public page is at /account-deletion on your frontend. Give that URL to Apple and Google, and check it describes what your deployment actually does: the prose and the code list the same fields and are kept in step by hand.

Enable Push Notifications in Xcode, or iOS push is dead

firebase_messaging ships and the app asks for notification permission at startup. On iOS, that is not enough: APNs needs the aps-environment entitlement, and the project has none until you add it.

Xcode target RunnerSigning & Capabilities+ CapabilityPush Notifications. That creates Runner.entitlements and points the build at it.

Nothing tells you if you skip it. The permission prompt still appears, the customer still agrees, the build still archives and still passes review — and no notification is ever delivered. The preflight refuses it once a GoogleService-Info.plist is present, which is the point at which you have said you want push.

Four steps nothing else will tell you about

Each of these fails quietly. Three of them produce a build that compiles, installs and runs, and is still ours in a way a reviewer can see.

Replacing the app icon does nothing on its own

assets/icons/app_icon.png is an input. What ships is what flutter_launcher_icons writes from it — the Android mipmaps under android/app/src/main/res/ and the iOS appiconset — and those are committed files that do not regenerate themselves.

So after you replace the source:

dart run flutter_launcher_icons

Skip it and your app carries our green mark on every home screen, while the source PNG sitting beside it is yours. The preflight checks both now, but it was possible to pass the old one and ship our icon.

Clear the iOS development team

ios/Runner.xcodeproj/project.pbxproj carries a DEVELOPMENT_TEAM value from our own Apple account. Open the project in Xcode, select the Runner target → Signing & Capabilities, and set the team to yours.

This one is fail-closed — Xcode cannot archive with a team you are not a member of — so it costs you an error rather than a rejection. It is here because the error does not say what it wants.

Changing the bundle identifier is a separate step from the name

The installer rewrites the display name. The identifier is what the stores key your app on and it is never guessed for you:

  • iOS — Xcode, Runner target → Signing & Capabilities → Bundle Identifier.
  • AndroidapplicationId in android/app/build.gradle.kts, and the package in the manifest and Kotlin source directory must match it.

Use a domain you control, reversed. The preflight refuses anything still under com.bicrypto — including the misspelt com.bicryptto, which is the one actually in the iOS project.

Bump the version between releases

version: in pubspec.yaml is name+build1.0.0+1. Both stores refuse an upload whose build number they have already seen, and the message arrives after a full release build has finished. Raise the number after + for every upload, and the part before it whenever the release is worth a version to a customer.

Before you submit

dart run tool/release_preflight.dart --explain refuses a build still pointing at a demo backend, still carrying our icon, still under our bundle identifier, or with no signing key configured.