How auth, data access, and API keys are protected — and what's intentionally not secret.
Every Flutter app ships its config to end users; anyone can extract strings from an APK / IPA / web bundle. The repo being public adds nothing to the attack surface as long as runtime keys are restricted in Google Cloud and Firestore rules enforce access. Hiding identifiers in source code is theatre. Hiding unrestricted API keys in source is malpractice — until rotation, those keys are usable by anyone who finds them.
FIREBASE_WEB_API_KEY,FIREBASE_ANDROID_API_KEY,FIREBASE_IOS_API_KEYYOUTUBE_API_KEY
Consumed at build time via --dart-define-from-file=.env (locally) or per-key --dart-define=KEY=... (CI). The Dart code reads them as compile-time constants via String.fromEnvironment(...) in lib/firebase_options.dart and lib/src/services/config_service.dart.
.env is not loaded at runtime and not bundled as a Flutter asset — earlier iterations of this repo used flutter_dotenv, which ships .env to build/web/assets/.env (publicly fetchable). Compile-time defines avoid that footgun. The values still end up in the minified JS / native binary, so GCP key restrictions remain the real safeguard (see below).
The YouTube API key has a secondary home in Remote Config (youtube_api_key). On mobile, ConfigService.getYouTubeApiKey() prefers .env but falls back to Remote Config — so the key can be rotated in production without shipping a new build.
android/app/google-services.json and ios/Runner/GoogleService-Info.plist are regenerated by flutterfire configure (or downloaded from the Firebase console). They contain the same Firebase API key as .env, plus OAuth client IDs and Android signing-cert SHA-1 hashes. *.example templates are committed for shape reference only.
projectId, messagingSenderId, appId, authDomain, storageBucket, measurementId, iosBundleId, the Android package name. Firebase docs classify these as identifiers; they appear in every built binary regardless. They're committed as plain string literals in firebase_options.dart.
Before flipping the repo public — or shipping any production build — set these in GCP Credentials:
| Key | Application restriction | API restriction |
|---|---|---|
FIREBASE_WEB_API_KEY |
HTTP referrers: production domain + localhost:* |
Identity Toolkit, Firebase, Firestore, Identity Platform, Token Service, Secure Token |
FIREBASE_ANDROID_API_KEY |
Android apps: com.tldrnews.app + debug + release SHA-1 |
Same set as web |
FIREBASE_IOS_API_KEY |
iOS apps: bundle ID com.tldrnews.app |
Same set as web |
YOUTUBE_API_KEY |
Whichever platform calls it (admin tooling) | YouTube Data API v3 only |
The YouTube key is the most dangerous — quota is billable. Restrict it tightly and monitor usage.
| Path | Read | Write |
|---|---|---|
accounts/{uid} |
self only | self only |
meta/{uid} |
any authenticated user | admin only |
channels/{cid} |
public | admin only |
{allPaths=**} |
admin | admin |
isAdmin() reads meta/{request.auth.uid}.admin. The wildcard rule grants admins blanket access; specific rules grant additional access to non-admins. Firestore evaluates rules as a logical OR, so combining specific + wildcard does what you'd expect.
Known caveat: if a user has no meta/{uid} doc at all, the get(...) inside isAdmin() will error, denying the request. That's safe by default but means new sign-ups need a meta/{uid} doc created before they can read anywhere that depends on isAdmin(). Bootstrap your first admin manually in the Firebase console.
firebase_app_check is in pubspec.yaml but not activated in code yet. Before production launch:
- Register attestation providers (Play Integrity on Android, DeviceCheck/AppAttest on iOS, reCAPTCHA Enterprise on web).
- Call
FirebaseAppCheck.instance.activate(...)inmain.dartafter Firebase init. - Enable enforcement in the Firebase console once you've verified non-attested clients are being rejected as expected.
App Check is the only mitigation that meaningfully reduces abuse from a stolen API key on rooted/jailbroken devices.
- All four keys rotated (
.envupdated, Firebase Remote Config updated, old keys deleted in GCP). - All four keys restricted (API + application restrictions).
- Firestore rules deployed (
firebase deploy --only firestore:rules). - No
AIzastrings in tracked files:git ls-files | xargs grep -nE 'AIza[0-9A-Za-z_-]{35}'returns nothing. -
.firebase/build cache is gitignored. - Fresh clone test: clone to
/tmp, follow setup.md, confirm app builds without any of the gitignored files present in the repo.
For security vulnerabilities, please email the maintainer rather than opening a public issue. Include a proof-of-concept and a suggested fix if you have one.