Firebase FCM Client

Pub Version License: MIT

Firebase Cloud Messaging (FCM) HTTP v1 API client for Dart with secure service account impersonation.

Send push notifications to devices, topics, and conditions without managing private keys.

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 - Battle-tested patterns

Installation

Add to pubspec.yaml:

dependencies:
  firebase_fcm_client: ^0.0.1

Then run:

dart pub get

Setup

1. Install Google Cloud SDK

Download and install from: https://cloud.google.com/sdk/docs/install

2. Authenticate with Google Cloud

gcloud auth application-default login

This opens a browser where you authenticate with your Google account and grants local credentials.

3. Get Service Account Email

Ask your Google Cloud administrator for the FCM service account email:

fcm-sa@your-project.iam.gserviceaccount.com

Or find it yourself:

gcloud iam service-accounts list

4. Verify Permissions

Ensure your account has the "Service Account Token Creator" role on the service account:

gcloud projects get-iam-policy YOUR_PROJECT \
  --flatten="bindings[].members" \
  --filter="bindings.members:YOUR_EMAIL@company.com"

Quick Start

Basic Example

import 'package:firebase_fcm_client/firebase_fcm_notification.dart';

void main() async {
  // Initialize with service account impersonation
  final auth = await ServiceAccountAuth.fromImpersonation(
    serviceAccountEmail: 'fcm-sa@your-project.iam.gserviceaccount.com',
    projectId: 'your-project-id',
  );

  final fcm = FCMClient(auth: auth, projectId: 'your-project-id');

  try {
    // Send notification to a device
    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

Service account authentication using Google Cloud impersonation.

Constructor

final auth = await ServiceAccountAuth.fromImpersonation({
  required String serviceAccountEmail,
  required String projectId,
});

Parameters:

  • serviceAccountEmail: Service account email (e.g., fcm-sa@project.iam.gserviceaccount.com)
  • projectId: Google Cloud project ID

Throws:

  • Exception if Application Default Credentials not configured

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

Send notification to a specific device:

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

Parameters:

  • deviceToken (required): Device registration token
  • title (required): Notification title
  • body (required): Notification body text
  • data (optional): Custom data key-value pairs (max 4KB)
  • android (optional): Android-specific configuration
  • webpush (optional): Web push configuration

Returns: Message ID string

Throws: FCMException on error


Send to Topic

Send notification to all devices subscribed to a topic:

final result = await fcm.sendToTopic(
  'news',
  title: 'Breaking News',
  body: 'Important update available',
  data: {
    'story_id': '12345',
    'priority': 'high',
  },
);

print('Sent to topic: $result');

Parameters:

  • topic (required): Topic name
  • title (required): Notification title
  • body (required): Notification body text
  • data (optional): Custom data

Send to Condition

Send notification based on complex conditions:

final result = await fcm.sendToCondition(
  "'sports' in topics && 'cricket' in topics",
  title: 'Cricket News',
  body: 'Latest updates',
);

print('Sent to condition: $result');

Condition Examples:

// Single topic
"'topic1' in topics"

// Multiple topics (OR)
"'topic1' in topics || 'topic2' in topics"

// Multiple topics (AND)
"'topic1' in topics && 'topic2' in topics"

// Complex
"('sports' in topics && 'cricket' in topics) || 'breaking_news' in topics"

Models

NotificationMessage

final message = NotificationMessage(
  token: 'device_token',  // OR topic/condition
  notification: NotificationPayload(
    title: 'Title',
    body: 'Body',
    imageUrl: 'https://example.com/image.png',
  ),
  data: {'key': 'value'},
  android: AndroidConfig(...),
  webpush: WebpushConfig(...),
);

AndroidConfig

final androidConfig = AndroidConfig(
  priority: 'high',  // 'high' or 'normal'
  notification: AndroidNotification(
    title: 'Custom Title',
    body: 'Custom Body',
    icon: 'ic_notification',
    color: '#FF5733',
    sound: 'default',
    priority: 1,
  ),
  collapseKey: 'message_group',
  ttl: 86400,  // Time to live in seconds
);

WebpushConfig

final webConfig = WebpushConfig(
  notification: WebpushNotification(
    title: 'Web Title',
    body: 'Web Body',
    icon: 'https://example.com/icon.png',
    badge: 'https://example.com/badge.png',
  ),
  data: {
    'url': 'https://example.com',
  },
);

Error Handling

FCMException

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

Error Cause Solution
ADC not configured Missing gcloud auth application-default login Run: gcloud auth application-default login
Permission denied Missing IAM role Ask admin to grant "Service Account Token Creator"
Invalid service account Wrong email format Verify email: fcm-sa@project.iam.gserviceaccount.com
Invalid token Device token expired Get fresh token from Firebase Messaging
Topic not found Invalid topic name Ensure devices are subscribed to topic

Examples

Send with Data Only (No Notification)

await fcm.sendToToken(
  deviceToken,
  title: '',
  body: '',
  data: {
    'action': 'open_settings',
    'screen': 'profile',
  },
);

Send to Multiple Topics (Condition)

await fcm.sendToCondition(
  "'notifications' in topics && 'offers' in topics",
  title: 'Special Offer',
  body: 'Limited time deal',
  data: {'offer_id': 'XYZ123'},
);

Send with Custom Android Configuration

await fcm.sendToToken(
  deviceToken,
  title: 'Urgent Alert',
  body: 'Action required',
  android: AndroidConfig(
    priority: 'high',
    notification: AndroidNotification(
      color: '#FF0000',
      sound: 'alarm',
      priority: 2,
    ),
    collapseKey: 'alert',
    ttl: 3600,
  ),
);

Send with Web Push Configuration

await fcm.sendToToken(
  deviceToken,
  title: 'Web Notification',
  body: 'Click to open',
  webpush: WebpushConfig(
    notification: WebpushNotification(
      title: 'Click Me',
      icon: 'https://example.com/icon.png',
      badge: 'https://example.com/badge.png',
    ),
    data: {
      'link': 'https://example.com/article',
    },
  ),
);

Send with Image

await fcm.sendToToken(
  deviceToken,
  title: 'Check This Out',
  body: 'Image notification',
  data: {},
  android: AndroidConfig(
    notification: AndroidNotification(
      icon: 'ic_notification',
    ),
  ),
);

Best Practices

1. Reuse ServiceAccountAuth

// Create once
final auth = await ServiceAccountAuth.fromImpersonation(...);

// Use multiple times
final fcm1 = FCMClient(auth: auth, projectId: 'project1');
final fcm2 = FCMClient(auth: auth, projectId: 'project2');

// Close when done
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));
    }
  }
  throw Exception('Failed after retries');
}

3. Validate Tokens Before Sending

bool isValidToken(String token) {
  return token.isNotEmpty && token.length > 50;
}

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
await fcm.sendToTopic('users', title: '...', body: '...');

Testing

Run unit tests:

dart test -r expanded

The package includes 24+ comprehensive tests covering:

  • Model serialization/deserialization
  • Message construction
  • Error handling
  • Complex message scenarios

Troubleshooting

"ADC not configured" Error

# Fix:
gcloud auth application-default login

"Permission denied" Error

Ask your Google Cloud admin to run:

gcloud projects add-iam-policy-binding YOUR_PROJECT \
  --member=user:YOUR_EMAIL@company.com \
  --role=roles/iam.serviceAccountTokenCreator \
  --condition=resource.name==serviceAccounts/FCM_SERVICE_ACCOUNT

Package Not Found

# Clear cache and reinstall
dart pub cache clean
dart pub get

Performance Notes

  • Token Caching: Access tokens are cached and automatically refreshed
  • Network: Uses HTTP keep-alive for connection reuse
  • Concurrency: Multiple requests can run concurrently

Security

  • No Private Keys: Uses service account impersonation
  • Automatic Token Refresh: Handles expiration transparently
  • Credential Isolation: Credentials never in code/config
  • Audit Trail: Google Cloud logs all access

Migration

From Private Key Method

// Old way (not recommended)
// final auth = ServiceAccountAuth.fromFile('key.json');

// New way (recommended)
final auth = await ServiceAccountAuth.fromImpersonation(
  serviceAccountEmail: 'fcm-sa@project.iam.gserviceaccount.com',
  projectId: 'your-project-id',
);

License

MIT License - See LICENSE file for details

Support

Contributing

Contributions are welcome! Please feel free to submit a pull request.

Changelog

See CHANGELOG.md for version history


Disclaimer

This package is designed for server-side/backend use. Do NOT embed service account credentials in client applications.


Made with ❤️ for Flutter and Dart developers