MMashDiv

App configuration — app_config.json

The app reads one JSON file that is packed into every build. Where it lives, why each change needs a new build, why a missing file looks like a server outage, what stops the app on its Configuration Error screen, what must never go in it, and a map of every key.

7 min readUpdated 26 September 2026mobile, configuration, app-config, baseurl, wsbaseurl, build, configuration-error

Almost everything you set in the app lives in one file, assets/config/app_config.json: the server it talks to, its name, identifiers and colours, the Google and Stripe client keys, and two trading defaults. You do not edit Dart code to configure the app.

A few things are deliberately not in this file:

  • Push notifications are switched on by Firebase's own files (google-services.json, GoogleService-Info.plist). See Push notifications.
  • The icon is an image, assets/icons/app_icon.png. See Branding.
  • Signing keys belong to the Android and iOS builds. See Build for Android and Build for iOS.
  • What the app offers each customer — which modules, which sign-in checks, which deposit gateways — is decided by your server's settings, not by the build.

The file and its template

File Whose it is What reads it
assets/config/app_config.json Yours. Edit this one. The app when it starts, and the Android and iOS builds
assets/config/app_config.example.json The template. Every release ships a new copy. The app, the Android build and the iOS build, only when app_config.json is missing or empty

The package you download contains an app_config.json that is a copy of the example, so a fresh unzip builds an app that points at the placeholder https://your-backend-url.com. Replace the placeholders in app_config.json before your first build; Install and run the app walks through the three values you need first.

An edit to the example does nothing while your own file exists, which is the usual reason for "I changed the config and nothing happened".

Every release you download carries the example again as its app_config.json. When you take an update, keep your own file and carry it into the new tree; see Updating your app.

It is packed into the app when you build

app_config.json is an asset. It is copied into the app when the app is built and read once, when the app starts. So:

  • Every change needs a new build. After editing the file, stop the app and run or build it again. Hot reload and hot restart do not read it again. You do not need flutter clean.
  • Some keys are also read by the build itself. Gradle reads appName and androidApplicationId on every Android build; an Xcode build phase reads appName on every iOS build; and dart run tool/apply_app_config.dart writes iosBundleId and iosTeamId into the Xcode project. Branding covers what each platform does with them.
  • A published app keeps the values it was built with. Nothing on your server can change them. Changing the backend address, a client key or a colour for customers means a new build, a store update, and waiting for customers to install it. The app has no forced-update mechanism, so an old build keeps calling the old address until its user updates. If you move your backend to a new domain, keep the old one answering until old builds are gone.

Only public values belong in it

The whole assets/config/ folder is packed into the app, and anyone who downloads your app can unpack the package and read it. Treat every value in the file as published.

Put in the file Never put in the file
Stripe publishable key (pk_…) Stripe secret key (sk_…), which lives in your server's .env
Google OAuth client ID Any OAuth client secret
Your server's public address Firebase service-account keys (FCM_*), which live in your server's .env

Do not leave other files in assets/config/, such as a backup of a config with real values in it. They are packed into the app too.

The example file carries the same warning as a note: _clientCredentialNotice. Keys that start with _ are notes for you; the app never reads them.

When the file is missing, the app looks like your server is down

If app_config.json is missing from the build, or is empty, the app does not stop. It quietly falls back to app_config.example.json and starts on the template's values, so every request goes to the placeholder https://your-backend-url.com. Sign-in and every screen after it fail, much as they would if your server were down, and no screen says why.

It is built this way on purpose. A project checked out from source control has no app_config.json, and the build does not notice, because the whole assets/config/ folder is bundled rather than named files. Falling back boots the same app a new buyer gets, instead of a crash on the first frame.

Three things do tell you:

  • The Android build ends its identity line with (assets/config/app_config.json is missing or empty, so the tracked template was used).
  • The iOS build shows a warning in Xcode's issue list: assets/config/app_config.json is missing or empty, so this build uses the template assets/config/app_config.example.json. It is a warning, not an error, so the build carries on.
  • The release preflight refuses the build: dart run tool/release_preflight.dart reports config-missing when the file is absent, and refuses the template's placeholder address when the file still carries it.

A quick check is the market list request in What a working first run looks like: if curl gets your markets from the address in your baseUrl but the app shows none, check first that the build contains your app_config.json.

The fallback covers a missing or empty file only. A file that exists but is broken never falls back; it stops the app, as below. Silently swapping your server for a placeholder because of a stray comma would be worse than a stop.

What stops the app: the Configuration Error screen

When the file cannot be used, the app does not start. It opens on a dark screen headed Configuration Error, with the line Failed to load app configuration, a box holding the exact problem, and this advice:

Fix the setting named above in assets/config/app_config.json. If that file does not exist yet, copy assets/config/app_config.example.json to it and set your own baseUrl and wsBaseUrl. Then stop the app and build again.

Nothing else runs behind it: no sign-in screen, no network. These are the causes, each named in the box:

Cause The box says
The file is not valid JSON: a trailing comma, a missing quote, a comment, curly quotes pasted from a document … is not valid JSON. followed by the parser's error
baseUrl or wsBaseUrl is missing or blank … does not set "baseUrl". (or "wsBaseUrl") with an example of what to write
A release build with a baseUrl that is not https://, or a wsBaseUrl that is not wss:// … sets "baseUrl" to "http://…", which is not a https:// address, and this is a release build. (or "wsBaseUrl" and wss://)
A colour that is not #RRGGBB or #AARRGGBB … sets "primaryColor" to "blue", which is not a colour.
appName is not text … sets "appName" to …, which is not text.
A value of the wrong JSON type, such as "false" in quotes for a switch or "3600" for a number … parsed, but a value in it has the wrong type.
Neither app_config.json nor the example could be read … Please ensure assets/config/app_config.json exists and is valid.

Debug and profile builds accept http:// and ws://, so you can test against a server on your own computer; see Testing against a server on your own computer. A release build is never allowed to send sign-in tokens in the clear. The screen scrolls when the message is longer than the phone's screen.

Every message and its fix is on Troubleshooting.

Write JSON values, not text

The app reads each key as a particular JSON type, and a value of another type stops it on the Configuration Error screen.

  • Text in double quotes: "baseUrl": "https://exchange.example.com".
  • Switches as bare true or false: "googleAuthEnabled": true. Not "true".
  • Numbers as bare whole numbers: "settingsCacheDuration": 3600. Not "3600", and not 3600.0.

A key the app does not know is ignored, whatever its value, so an older config that still carries keys this release no longer reads keeps working. The configuration reference lists them. You can delete those lines, but you do not have to.

Check it before you build

Run this from the project root, after flutter pub get:

dart run tool/apply_app_config.dart --check

It changes nothing. It reports every problem it finds at once, and exits 1 if there is one, or if the Xcode project's bundle id or signing team differs from a set iosBundleId or iosTeamId. It checks that the file is valid JSON, that baseUrl and wsBaseUrl are set and are http(s):// and ws(s):// addresses, and the name, identifier, team and colour keys.

An http:// or ws:// address is only a warning in that run, because a debug build accepts it. Add --release before a build you will publish, and it becomes an error, as it is for the app:

dart run tool/apply_app_config.dart --check --release

It does not check the type of the other keys: a switch written as "false" in quotes passes the check and still stops the app.

An Android build is the second check: Gradle parses the same file on every build and stops, naming the line and column, if it is not valid JSON.

The keys, grouped

baseUrl and wsBaseUrl are required. Every other key is optional and has a default.

Group Keys Where it is explained
Your server (required) baseUrl, wsBaseUrl Install and the reference
Name, identifiers and colours appName, androidApplicationId, iosBundleId, iosTeamId, primaryColor, buyColor, sellColor, accentColor Branding
Google sign-in googleServerClientId, googleAuthEnabled Sign-in methods
Card deposits stripePublishableKey Payments
Trading defaults defaultTradingPair, defaultExchangeProvider Reference
Housekeeping appVersion, settingsCacheDuration, _clientCredentialNotice Reference

The configuration reference has every key with its type, default, effect, when it takes effect, and the server setting it has to agree with.

A minimal file

{
  "baseUrl": "https://exchange.example.com",
  "wsBaseUrl": "wss://exchange.example.com",
  "appName": "MyExchange"
}

This is a working configuration: every key it leaves out takes its default. Before you publish, add at least your own identifiers and colours, because the release preflight refuses a build that still carries the template's; see Branding.

What it deliberately does not do

  • No remote configuration. The app never downloads its own configuration. Values in this file change only with a new build.
  • No per-environment files. There is one file. To build against a staging server, point baseUrl and wsBaseUrl at it, build, and put your production values back before the release build.
  • No branding from your server. The logos and colours on your website do not reach the app. Its name, colours and icon come only from the build.
  • No server switches. Which sign-in checks run, which modules show and which payment gateways appear are your server's settings. The keys in this file cannot turn on something your server has off. The one exception is the Continue with Google button: the app shows it according to googleAuthEnabled (or, when that key is absent, whether googleServerClientId is set), not your server's Google OAuth Login switch; see Sign-in methods.