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.
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
appNameandandroidApplicationIdon every Android build; an Xcode build phase readsappNameon every iOS build; anddart run tool/apply_app_config.dartwritesiosBundleIdandiosTeamIdinto 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.dartreportsconfig-missingwhen 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
trueorfalse:"googleAuthEnabled": true. Not"true". - Numbers as bare whole numbers:
"settingsCacheDuration": 3600. Not"3600", and not3600.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 --checkIt 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 --releaseIt 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
baseUrlandwsBaseUrlat 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, whethergoogleServerClientIdis set), not your server's Google OAuth Login switch; see Sign-in methods.