← Back to posts Cover image for Securing Your Flutter App: Best Practices for Storing API Keys and Sensitive Data

Securing Your Flutter App: Best Practices for Storing API Keys and Sensitive Data

· 5 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

The Problem: Secrets in Plain Sight

We’ve all done it. You’re integrating a new service into your Flutter app—maybe a payment gateway, a mapping API, or a cloud database. You get that API key, and in the rush to see it work, you paste it directly into your Dart code:

// ⚠️ DON'T DO THIS ⚠️
class ApiService {
  static const String apiKey = 'sk_live_abc123xyz789';
  // Rest of your service...
}

You push your code, the feature works, and you move on. The problem? That secret is now hard-coded, readable by anyone with access to your codebase, and if you accidentally commit it to a public repository, it’s exposed to the entire world. Attackers can find these keys using automated scanners in minutes, leading to unauthorized API usage, data breaches, and hefty bills.

The core issue is that mobile apps are inherently exposed—their compiled code can be reverse-engineered. While we can’t achieve perfect security, we can implement layers of protection that make extraction impractical and prevent accidental leaks.

Solution 1: Environment Variables with .env Files

The most straightforward improvement is to remove secrets from your source code entirely using environment variables. The flutter_dotenv package is perfect for this.

First, add the dependency:

dependencies:
  flutter_dotenv: ^5.1.0

Create a .env file in your project root:

# .env
MAPS_API_KEY=your_actual_key_here
STRIPE_SECRET_KEY=sk_test_abc123
BACKEND_BASE_URL=https://api.yourservice.com

Crucially, add .env to your .gitignore file immediately:

# .gitignore
.env

Create a .env.example file (committed to version control) that shows the required structure without real values:

# .env.example
MAPS_API_KEY=your_maps_api_key_here
STRIPE_SECRET_KEY=your_stripe_secret_key_here
BACKEND_BASE_URL=your_backend_base_url_here

Load and use the environment variables in your app:

import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  await dotenv.load(fileName: '.env');
  runApp(MyApp());
}

class LocationService {
  String get mapsApiKey => dotenv.get('MAPS_API_KEY');
  
  Future<void> fetchLocation() async {
    final response = await http.get(
      Uri.parse('https://maps.api.com/endpoint?key=${mapsApiKey}'),
    );
    // Process response...
  }
}

This approach keeps secrets out of your codebase, but remember: the .env file is still bundled with your app. For production, we need stronger measures.

Solution 2: Flutter Flavors for Configuration Management

Flavors (or build configurations) let you maintain different versions of your app (development, staging, production) with separate configurations. This is ideal for managing different API endpoints and keys per environment.

Define flavors in your android/app/build.gradle:

android {
    flavorDimensions "environment"
    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
        }
        prod {
            dimension "environment"
        }
    }
}

Create configuration files for each environment:

// lib/config/dev_config.dart
class AppConfig {
  static const String apiBaseUrl = 'https://dev-api.example.com';
  static const String analyticsKey = 'dev_key_123';
}

// lib/config/prod_config.dart  
class AppConfig {
  static const String apiBaseUrl = 'https://api.example.com';
  static const String analyticsKey = 'prod_key_456';
}

Use conditional imports in your main file:

// lib/main.dart
import 'config.dart';

void main() => runApp(MyApp());

// lib/config.dart (gateway file)
import 'flavor.dart'; // This import is unnecessary and incorrect; removed.

// Conditional import based on compile-time variable
const flavor = String.fromEnvironment('FLAVOR');

if (flavor == 'dev') {
  import 'config/dev_config.dart' as config;
} else {
  import 'config/prod_config.dart' as config;
}

class AppConfig {
  static final String apiBaseUrl = config.AppConfig.apiBaseUrl;
  static final String analyticsKey = config.AppConfig.analyticsKey;
}

Build with a specific flavor:

flutter run --flavor dev --dart-define=FLAVOR=dev

Solution 3: Native Secure Storage for Runtime Secrets

For secrets that need to be stored on the device (like user tokens or keys downloaded after authentication), use platform-specific secure storage. The flutter_secure_storage package provides a cross-platform interface to Keychain (iOS) and Keystore (Android).

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorageService {
  static const _storage = FlutterSecureStorage();
  
  static Future<void> saveApiKey(String key) async {
    await _storage.write(key: 'user_api_key', value: key);
  }
  
  static Future<String?> getApiKey() async {
    return await _storage.read(key: 'user_api_key');
  }
  
  static Future<void> deleteApiKey() async {
    await _storage.delete(key: 'user_api_key');
  }
}

// Usage
await SecureStorageService.saveApiKey('downloaded_user_key');
final key = await SecureStorageService.getApiKey();

For Android, configure your android/app/build.gradle to enable secure storage:

android {
    defaultConfig {
        minSdkVersion 23 // Required for Android Keystore system
    }
}

Solution 4: Backend Proxy for Ultimate Security

The most secure approach is to never store third-party API keys in your mobile app at all. Instead, create your own backend service that acts as a proxy:

  1. Your app authenticates with your backend
  2. Your backend stores the third-party API keys securely (using environment variables or secret management services)
  3. Your app makes requests to your backend, which forwards them to the third-party service

This way, even if someone reverse-engineers your app, they only get your backend endpoint (which should have proper authentication and rate limiting).

Best Practices Summary

  1. Never commit secrets to version control—use .gitignore religiously
  2. Use environment variables for development convenience
  3. Implement flavors to manage different environments
  4. Store device-specific secrets in secure storage
  5. Consider a backend proxy for sensitive third-party integrations
  6. Rotate your keys regularly and have a revocation plan
  7. Use different keys for development and production environments

Remember, mobile app security is about raising the cost of extraction to impractical levels. By combining these techniques—using .env files during development, flavors for environment separation, secure storage for runtime data, and backend proxies for sensitive operations—you create a defense-in-depth approach that protects both your data and your users.

Start implementing these practices today. Your future self (and your users) will thank you when you avoid that midnight emergency key rotation.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter's Hidden Gem: Building Powerful Linux Desktop Apps with Ease

Flutter is gaining traction as a robust framework for Linux desktop development, offering a smoother experience compared to traditional toolkits like GTK and Qt. This post will explore why Flutter excels for Linux apps, highlight its advantages, and provide practical insights for developers looking to leverage Flutter for their desktop projects.

Cover image for Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Simplifying Flutter Desktop Deployment: Signing and Distribution for Windows

Deploying Flutter desktop apps on Windows can be challenging, especially regarding code signing and preventing Windows from blocking installations. This post will guide developers through the process of signing their Flutter desktop applications, exploring alternatives to the Microsoft Store like creating executable installers, and covering the necessary steps to ensure a smooth user experience.

Cover image for Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Mastering Flutter Tooling: Streamlining SDK Management and Installation on Windows

Developers frequently voice frustrations about Flutter SDK version management and installation processes, particularly on Windows. This post will explore best practices for managing multiple Flutter versions using tools like FVM, discuss integrating Flutter with package managers like Winget for a smoother Windows setup, and offer strategies to ensure a consistent development environment across projects and teams.