Firebase FCM Client
Firebase Cloud Messaging (FCM) HTTP v1 API client for Dart with production-ready authentication.
Send push notifications to devices, topics, and conditions without managing private keys.
What's New in v0.0.3
Production-Ready Authentication
- Works in Cloud Run, GKE, Docker, Kubernetes
- No
gcloudCLI needed in production - Multiple auth methods automatically detected
Enhanced Security
- Uses
googleapis_authfor secure token handling - Automatic token refresh before expiry
- No credentials in code
Improved Reliability
- Supports GOOGLE_APPLICATION_CREDENTIALS environment variable
- Fallback authentication methods
- Better error messages for debugging
Features
- Send to Device Tokens - Target specific devices
- Send to Topics - Broadcast to subscribed devices
- Send to Conditions - Complex targeting with logical operators
- Service Account Impersonation - Secure authentication, no private keys
- Android Configuration - Customize Android-specific behavior
- Web Push Configuration - Web-specific notification settings
- Automatic Token Caching - Efficient token management
- Automatic Token Refresh - Handles expiration transparently
- Complete Error Handling - Detailed error messages
- Full Type Safety - 100% Dart type-safe
- Enterprise-Grade Security - No credentials in code
- Production Ready - Works in all environments
Installation
Add to pubspec.yaml:
dependencies:
firebase_fcm_client: ^0.0.3
Then run:
dart pub get
Setup (Choose One Method)
Method 1: Cloud Run / GKE (Recommended)
# 1. Create service account
gcloud iam service-accounts create intellitoggle-fcm \
--display-name="IntelliToggle FCM Service"
# 2. Grant permissions
gcloud projects add-iam-policy-binding PROJECT_ID \
--member=serviceAccount:intellitoggle-fcm@PROJECT.iam.gserviceaccount.com \
--role=roles/firebase.cloudmessagingServiceAgent
# 3. Deploy with service account
gcloud run deploy my-backend \
--source . \
--service-account=intellitoggle-fcm@PROJECT.iam.gserviceaccount.com
Result: Works automatically! No env vars needed.
Method 2: Docker / Kubernetes
# 1. Create service account JSON key
gcloud iam service-accounts keys create key.json \
--iam-account=intellitoggle-fcm@PROJECT.iam.gserviceaccount.com
# 2. Keep it safe
echo "key.json" >> .gitignore
docker-compose.yml:
environment:
- GOOGLE_APPLICATION_CREDENTIALS=/secrets/service-account.json
volumes:
- ./key.json:/secrets/service-account.json:ro
Kubernetes:
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /var/secrets/fcm/service-account.json
volumeMounts:
- name: fcm-secret
mountPath: /var/secrets/fcm
Method 3: Local Development
gcloud auth application-default login
Then run:
dart run bin/main.dart
Quick Start
Basic Example
import 'package:firebase_fcm_client/firebase_fcm_client.dart';
void main() async {
// Works in all environments:
// - Cloud Run (automatic)
// - GKE (automatic)
// - Docker (via GOOGLE_APPLICATION_CREDENTIALS)
// - Local dev (via gcloud auth)
final auth = await ServiceAccountAuth.fromImpersonation(
serviceAccountEmail: 'intellitoggle-fcm@project.iam.gserviceaccount.com',
projectId: 'intellitoggle-project',
);
final fcm = FCMClient(auth: auth, projectId: 'intellitoggle-project');
try {
// Send notification
final result = await fcm.sendToToken(
'device_token_here',
title: 'Hello!',
body: 'This is a test notification',
);
print('Message sent: $result');
await auth.close();
} catch (e) {
print('Error: $e');
}
}
API Reference
ServiceAccountAuth
Handles authentication for Firebase Cloud Messaging.
Initialization
final auth = await ServiceAccountAuth.fromImpersonation({
required String serviceAccountEmail,
required String projectId,
});
How it works (automatic):
- Tries googleapis_auth (works in Cloud Run, GKE, local gcloud)
- Tries GOOGLE_APPLICATION_CREDENTIALS env var (Docker, Kubernetes)
- Falls back to gcloud CLI (local dev)
Throws:
Exceptionif all auth methods fail
Methods
// Get access token (cached automatically)
Future<String> getAccessToken()
// Close resources
Future<void> close()
FCMClient
Firebase Cloud Messaging HTTP v1 API client.
Constructor
final fcm = FCMClient({
required ServiceAccountAuth auth,
required String projectId,
});
Send to Device Token
final result = await fcm.sendToToken(
'device_token',
title: 'Title',
body: 'Body text',
data: {
'key1': 'value1',
'key2': 'value2',
},
android: AndroidConfig(
priority: 'high',
notification: AndroidNotification(
color: '#FF5733',
sound: 'default',
),
),
webpush: WebpushConfig(
notification: WebpushNotification(
title: 'Web Title',
icon: 'https://example.com/icon.png',
),
),
);
print('Sent: $result'); // Message ID
Send to Topic
final result = await fcm.sendToTopic(
'news',
title: 'Breaking News',
body: 'Important update available',
data: {
'story_id': '12345',
},
);
Send to Condition
final result = await fcm.sendToCondition(
"'sports' in topics && 'cricket' in topics",
title: 'Cricket News',
body: 'Latest updates',
);
Authentication Flow
┌─ serviceAccountEmail: 'fcm-sa@project.iam.gserviceaccount.com'
│
└─ ServiceAccountAuth.fromImpersonation()
│
├─ Try 1: googleapis_auth + Application Default Credentials
│ ├─ Works in: Cloud Run
│ ├─ Works in: GKE
│ ├─ Works in: Local (gcloud auth)
│ └─ Success → Use this
│
├─ Try 2: GOOGLE_APPLICATION_CREDENTIALS environment variable
│ ├─ Works in: Docker
│ ├─ Works in: Kubernetes
│ ├─ Requires: env var set + JSON file mounted
│ └─ Success → Use this
│
├─ Try 3: gcloud CLI (fallback)
│ ├─ Works in: Local dev
│ ├─ Requires: gcloud installed + auth setup
│ └─ Success → Use this
│
└─ All failed → Throw helpful error
Error Handling
try {
await fcm.sendToToken(token, title: '...', body: '...');
} on FCMException catch (e) {
print('Error: ${e.message}');
print('Code: ${e.code}');
print('Status: ${e.statusCode}');
}
Common Errors & Solutions
| Error | Cause | Solution |
|---|---|---|
All authentication methods failed |
No auth setup | Use one of the 3 setup methods |
GOOGLE_APPLICATION_CREDENTIALS not set |
Docker/K8s issue | Set env var and mount secret |
gcloud command failed |
gcloud CLI missing | Run gcloud auth application-default login |
Invalid token |
Device token expired | Get fresh token from Firebase Messaging |
Permission denied |
Missing IAM role | Ask admin to grant proper role |
Examples
Real-World
class NotificationService {
late FCMClient _fcm;
late ServiceAccountAuth _auth;
Future<void> initialize() async {
_auth = await ServiceAccountAuth.fromImpersonation(
serviceAccountEmail: 'intellitoggle-fcm@project.iam.gserviceaccount.com',
projectId: 'intellitoggle-project',
);
_fcm = FCMClient(auth: _auth, projectId: 'intellitoggle-project');
}
Future<void> notifyFlagToggle({
required String userId,
required String deviceToken,
required String flagName,
required bool newValue,
}) async {
await _fcm.sendToToken(
deviceToken,
title: '$flagName was toggled ${newValue ? "ON" : "OFF"}',
body: 'Feature flag changed in production',
data: {
'actionType': 'feature-flag-update',
'flagName': flagName,
'newValue': newValue.toString(),
},
);
}
Future<void> shutdown() async {
await _auth.close();
}
}
Send with Custom Android Config
await fcm.sendToToken(
deviceToken,
title: 'Urgent Alert',
body: 'Action required',
android: AndroidConfig(
priority: 'high',
notification: AndroidNotification(
color: '#FF0000',
sound: 'alarm',
priority: 2,
),
),
);
Send to Topic
await fcm.sendToTopic(
'feature-updates',
title: 'New Feature Available',
body: 'Check out our latest update',
);
Best Practices
1. Reuse ServiceAccountAuth
// Create once at startup
final auth = await ServiceAccountAuth.fromImpersonation(...);
// Reuse for all FCM clients
final fcm1 = FCMClient(auth: auth, projectId: 'project1');
final fcm2 = FCMClient(auth: auth, projectId: 'project2');
// Close when shutting down
await auth.close();
2. Implement Retry Logic
Future<String> sendWithRetry(String token, String title, String body) async {
int retries = 3;
while (retries > 0) {
try {
return await fcm.sendToToken(token, title: title, body: body);
} catch (e) {
retries--;
if (retries == 0) rethrow;
await Future.delayed(Duration(seconds: 2 ^ (3 - retries)));
}
}
throw Exception('Failed after retries');
}
3. Validate Tokens Before Sending
bool isValidToken(String token) {
return token.isNotEmpty && token.length > 100;
}
if (isValidToken(deviceToken)) {
await fcm.sendToToken(deviceToken, title: '...', body: '...');
}
4. Use Topics for Bulk Messaging
// Instead of looping through tokens
for (final token in tokens) {
await fcm.sendToToken(token, title: '...', body: '...');
}
// Use topics (more efficient)
await fcm.sendToTopic('users', title: '...', body: '...');
Testing
Unit Tests
dart test test/firebase_fcm_notification_test.dart
//or
dart test -r expanded
Tests cover:
- Model serialization/deserialization
- Message construction
- Error handling
- Complex message scenarios
Integration Test
# Setup auth first
gcloud auth application-default login
# Run full test
dart test
Migration from v0.0.2 to v0.0.3
No breaking changes to your code! Just update the package version:
dependencies:
firebase_fcm_client: ^0.0.3
Your existing code works as-is:
final auth = await ServiceAccountAuth.fromImpersonation(
serviceAccountEmail: 'fcm-sa@project.iam.gserviceaccount.com',
projectId: 'your-project-id',
);
The difference: v0.0.3 now works in production environments (Cloud Run, Docker, K8s)!
Troubleshooting
Test Authentication
# Test Cloud Run/GKE auth
gcloud auth application-default login
dart run bin/main.dart
# Should print: Using Application Default Credentials
Check Environment Variable
# Docker/Kubernetes
echo $GOOGLE_APPLICATION_CREDENTIALS # Should print path
ls -la /path/to/service-account.json # Should exist
Debug Logs
// Enable detailed logging
final auth = await ServiceAccountAuth.fromImpersonation(
serviceAccountEmail: 'fcm-sa@project.iam.gserviceaccount.com',
projectId: 'intellitoggle-project',
);
// Will print which auth method is being used
final token = await auth.getAccessToken();
Performance
- Token Caching: Tokens cached and reused until expiration
- Automatic Refresh: Tokens refreshed transparently before expiry
- Connection Pooling: HTTP keep-alive enabled
- Concurrent Requests: Multiple messages can be sent in parallel
Security
No Private Keys: Uses secure service account impersonation
Automatic Token Refresh: Credentials never exposed
Credential Isolation: No secrets in code/config
Audit Trail: All operations logged in Google Cloud
Support
- Issues: GitHub Issues
- Documentation: Check code comments for detailed explanations
- Examples: See examples/ directory for complete implementations
License
MIT License - See LICENSE file for details
Changelog
See CHANGELOG.md for version history
Made with ❤️ for Dart and Flutter developers