Most Flutter tutorials about hiding API keys either push you toward Firebase Remote Config or AWS Secrets Manager before you have shipped anything. For prototypes and side projects, that is overkill. A simple JSON file in your assets, loaded once at boot through a singleton, gets you 90% of the way there with zero extra dependencies.
At a glance
- Store secret values in `assets/config/secrets.json`.
- Register the file under `flutter:assets:` in `pubspec.yaml`.
- Load it once at startup through a `SecretsManager` singleton.
- Add the file to `.gitignore` so it never enters version control.
- Use separate JSON files for dev, staging, and prod when the project grows.
How the pattern works
A singleton class wraps three pieces of state: the API base URL, the API key, and the Firebase project ID. The class exposes async getters that lazily load from the bundled JSON file on first access. From there, the rest of the app calls `SecretsManager().apiKey` and forgets about it.
The class also guards re-initialization. If something calls it before `main()` has finished loading, the singleton waits, then serves the value once the JSON has been parsed. That avoids the classic "I tried to read the key during widget build and got an empty string" bug.
Why it matters
This pattern has a sharp tradeoff: simple for solo developers, fragile at scale.
Who wins:** The solo builder who needs a weather API key for a two-day prototype. No CI configuration, no package overhead, no onboarding docs. The singleton shape also survives the transition to a real secret store—the calls stay the same even when the loading logic changes.
Who loses:** Teams of two or more. Once `secrets.json` hits `.gitignore`, every new developer must manually place a private file before the app runs. That manual step works for a week, then someone commits their key by accident, then the team is rotating credentials and updating onboarding docs anyway. At that point, build-time injection (a CI variable passed via `String.fromEnvironment`) costs less than the workflow overhead this approach creates.
The real risk:** Bundled secrets are not secrets—they are configuration. Any user who downloads your APK can unzip it and read the JSON file with standard tools. This is not a security boundary; it is a convenience boundary. Treat it accordingly.
Who should care?
| Situation | Recommended approach |
|---|---|
| Solo prototype, live < 1 week | JSON singleton (this guide) |
| Solo project with reuse potential | JSON singleton now, env vars later |
| Team of 2+ | Build-time injection from day one |
| App going to public store | Any external secret service |
| Client work with NDA | Build-time injection + signed config |
Worked example
Say you are a product designer building a Flutter demo of a weather app to show off a sprint concept. You need an OpenWeather API key, a base URL, and a project ID.
You create `assets/config/secrets.json`, register it in `pubspec.yaml`, drop the singleton class into `lib/config/secrets_manager.dart`, and call `SecretsManager().load()` from `main()` before `runApp()`. The demo runs. The next person who clones the repo pastes their own JSON file in place and is unblocked.
The moment the demo graduates into a real product, swap the JSON load for environment variables injected at build time. The singleton API can stay identical.
What to watch next
Watch Flutter's `String.fromEnvironment` tooling for build-time injection and the `flutter_dotenv` package if you want a more established option with less boilerplate. If your CI already has secret injection (GitHub Actions, GitLab CI, etc.), use it—do not maintain a manual JSON file across machines.
Key takeaways
- A singleton-backed `secrets.json` is the simplest no-package way to manage API keys in Flutter.
- Load once in `main()` and expose async getters that auto-initialize on first use.
- Add the file to `.gitignore` immediately and rotate any key that ever lands in source control.
- This pattern works for prototypes; for teams or production, use build-time environment variable injection instead.
Frequently asked questions
Where do I put the secrets.json file in Flutter?
Inside your project under `assets/config/secrets.json`, and list it under the `flutter:assets:` section of `pubspec.yaml` so it ships with the app.
Is storing API keys in assets secure?
No. Bundled secrets can be extracted from any installed APK. Use this pattern only for prototypes and demos, not for production apps.
Why use a singleton class for secrets?
It guarantees one load per app lifecycle, exposes async getters that auto-initialize, and avoids passing the secrets object through every widget tree.
Should I add secrets.json to .gitignore?
Yes. Anyone who clones the repo should paste their own local secrets file rather than pulling yours from version control.