Write Firebase Cloud Functions in Dart with full type safety and performance.
This package provides a complete Dart implementation of Firebase Cloud Functions with support for:
| Trigger Type | Status | Functions |
|---|---|---|
| HTTPS | ✅ Complete | onRequest, onCall, onCallWithData |
| Pub/Sub | ✅ Complete | onMessagePublished |
| Firestore | ✅ Complete | onDocumentCreated, onDocumentUpdated, onDocumentDeleted, onDocumentWritten, onDocumentCreatedWithAuthContext, onDocumentUpdatedWithAuthContext, onDocumentDeletedWithAuthContext, onDocumentWrittenWithAuthContext |
| Realtime Database | ✅ Complete | onValueCreated, onValueUpdated, onValueDeleted, onValueWritten |
| Storage | ✅ Complete | onObjectFinalized, onObjectArchived, onObjectDeleted, onObjectMetadataUpdated |
| Scheduler | ✅ Complete | onSchedule |
| Firebase Alerts | ✅ Complete | onInAppFeedbackPublished, onNewAnrIssuePublished, onNewFatalIssuePublished, onNewNonfatalIssuePublished, onNewTesterIosDevicePublished, onPlanAutomatedUpdatePublished, onPlanUpdatePublished, onRegressionAlertPublished, onStabilityDigestPublished, onThresholdAlertPublished, onVelocityAlertPublished |
| Eventarc | ✅ Complete | onCustomEventPublished |
| Identity Platform | ✅ Complete | beforeUserCreated, beforeUserSignedIn (+ beforeEmailSent, beforeSmsSent*) |
| Remote Config | ✅ Complete | onConfigUpdated |
| Test Lab | ✅ Complete | onTestMatrixCompleted |
- Features
- Prerequisites
- Installation
- Quick Start
- HTTPS Functions
- Pub/Sub Triggers
- Firestore Triggers
- Realtime Database Triggers
- Storage Triggers
- Scheduler Triggers
- Firebase Alerts
- Identity Platform (Auth Blocking)
- Remote Config
- Test Lab
- Parameters & Configuration
- Project Configuration
- Development
- Type-safe: Leverage Dart's strong type system with typed callable functions and CloudEvents
- Fast: Compiled Dart code with efficient Shelf HTTP server
- Familiar API: Similar to Firebase Functions Node.js SDK v2
- Streaming: Server-Sent Events (SSE) support for callable functions
- Parameterized: Deploy-time configuration with
defineString,defineInt,defineBoolean - Conditional Config: CEL expressions for environment-based options
- Error Handling: Built-in typed error classes matching the Node.js SDK
- Hot Reload: Fast development with build_runner watch
- Dart SDK >=3.0.0
- Node v22 (Later versions of Node won't work!)
- Java 21+ (for Firestore)
- Custom Firebase CLI with Dart runtime support:
git clone -b @invertase/dart https://github.com/invertase/firebase-tools.git
cd firebase-tools
npm install
npm run build
npm linkAdd to your pubspec.yaml:
dependencies:
firebase_functions:
path: ../firebase-functions-dartimport 'package:firebase_functions/firebase_functions.dart';
void main(List<String> args) {
fireUp(args, (firebase) {
// Register your functions here
});
}firebase.https.onRequest(
name: 'hello',
(request) async {
return Response.ok('Hello from Dart!');
},
);firebase.https.onCall(
name: 'greet',
(request, response) async {
final data = request.data as Map<String, dynamic>?;
final name = data?['name'] ?? 'World';
return CallableResult({'message': 'Hello, $name!'});
},
);firebase.https.onCallWithData<GreetRequest, GreetResponse>(
name: 'greetTyped',
fromJson: GreetRequest.fromJson,
(request, response) async {
return GreetResponse(message: 'Hello, ${request.data.name}!');
},
);firebase.https.onCall(
name: 'countdown',
options: const CallableOptions(
heartBeatIntervalSeconds: HeartBeatIntervalSeconds(5),
),
(request, response) async {
if (request.acceptsStreaming) {
for (var i = 10; i >= 0; i--) {
await response.sendChunk({'count': i});
await Future.delayed(Duration(milliseconds: 100));
}
}
return CallableResult({'message': 'Countdown complete!'});
},
);firebase.https.onCall(
name: 'divide',
(request, response) async {
final data = request.data as Map<String, dynamic>?;
final a = data?['a'] as num?;
final b = data?['b'] as num?;
if (a == null || b == null) {
throw InvalidArgumentError('Both "a" and "b" are required');
}
if (b == 0) {
throw FailedPreconditionError('Cannot divide by zero');
}
return CallableResult({'result': a / b});
},
);Available error types: InvalidArgumentError, FailedPreconditionError, NotFoundError, AlreadyExistsError, PermissionDeniedError, ResourceExhaustedError, UnauthenticatedError, UnavailableError, InternalError, DeadlineExceededError, CancelledError.
firebase.pubsub.onMessagePublished(
topic: 'my-topic',
(event) async {
final message = event.data;
print('ID: ${message?.messageId}');
print('Data: ${message?.textData}');
print('Attributes: ${message?.attributes}');
},
);// Document created
firebase.firestore.onDocumentCreated(
document: 'users/{userId}',
(event) async {
final data = event.data?.data();
print('Created: users/${event.params['userId']}');
print('Name: ${data?['name']}');
},
);
// Document updated
firebase.firestore.onDocumentUpdated(
document: 'users/{userId}',
(event) async {
final before = event.data?.before?.data();
final after = event.data?.after?.data();
print('Before: $before');
print('After: $after');
},
);
// Document deleted
firebase.firestore.onDocumentDeleted(
document: 'users/{userId}',
(event) async {
final data = event.data?.data();
print('Deleted data: $data');
},
);
// All write operations
firebase.firestore.onDocumentWritten(
document: 'users/{userId}',
(event) async {
final before = event.data?.before?.data();
final after = event.data?.after?.data();
// Determine operation type
if (before == null && after != null) print('CREATE');
if (before != null && after != null) print('UPDATE');
if (before != null && after == null) print('DELETE');
},
);
// Nested collections
firebase.firestore.onDocumentCreated(
document: 'posts/{postId}/comments/{commentId}',
(event) async {
print('Post: ${event.params['postId']}');
print('Comment: ${event.params['commentId']}');
},
);
// With auth context (identifies the principal that triggered the write)
firebase.firestore.onDocumentCreatedWithAuthContext(
document: 'orders/{orderId}',
(event) async {
print('Auth type: ${event.authType}');
print('Auth ID: ${event.authId}');
final data = event.data?.data();
print('Order: ${data?['product']}');
},
);
firebase.firestore.onDocumentUpdatedWithAuthContext(
document: 'orders/{orderId}',
(event) async {
print('Updated by: ${event.authType} (${event.authId})');
final before = event.data?.before?.data();
final after = event.data?.after?.data();
print('Before: $before');
print('After: $after');
},
);
firebase.firestore.onDocumentDeletedWithAuthContext(
document: 'orders/{orderId}',
(event) async {
print('Deleted by: ${event.authType} (${event.authId})');
final data = event.data?.data();
print('Deleted data: $data');
},
);
firebase.firestore.onDocumentWrittenWithAuthContext(
document: 'orders/{orderId}',
(event) async {
print('Written by: ${event.authType} (${event.authId})');
final before = event.data?.before;
final after = event.data?.after;
if (before == null || !before.exists) print('CREATE');
else if (after == null || !after.exists) print('DELETE');
else print('UPDATE');
},
);Respond to changes in Firebase Realtime Database. The ref parameter supports path wildcards (e.g., {messageId}) which are extracted into event.params.
| Function | Triggers when | Event data |
|---|---|---|
onValueCreated |
Data is created | DataSnapshot? |
onValueUpdated |
Data is updated | Change<DataSnapshot>? (before/after) |
onValueDeleted |
Data is deleted | DataSnapshot? (deleted data) |
onValueWritten |
Any write (create/update/delete) | Change<DataSnapshot>? (before/after) |
The DataSnapshot class provides methods to inspect the data:
val()— Returns the snapshot contents (Map, List, String, num, bool, or null)exists()— Returnstrueif the snapshot contains datachild(path)— Gets a child snapshot at the given pathhasChild(path)/hasChildren()— Check for child datanumChildren()— Number of child propertieskey— Last segment of the reference path
// Value created
firebase.database.onValueCreated(
ref: 'messages/{messageId}',
(event) async {
final data = event.data?.val();
print('Created: ${event.params['messageId']}');
print('Data: $data');
print('Instance: ${event.instance}');
},
);
// Value updated — access before/after states
firebase.database.onValueUpdated(
ref: 'messages/{messageId}',
(event) async {
final before = event.data?.before?.val();
final after = event.data?.after?.val();
print('Before: $before');
print('After: $after');
},
);
// Value deleted
firebase.database.onValueDeleted(
ref: 'messages/{messageId}',
(event) async {
final data = event.data?.val();
print('Deleted: $data');
},
);
// All write operations — determine operation type from before/after
firebase.database.onValueWritten(
ref: 'users/{userId}/status',
(event) async {
final before = event.data?.before;
final after = event.data?.after;
if (before == null || !before.exists()) print('CREATE');
else if (after == null || !after.exists()) print('DELETE');
else print('UPDATE');
},
);Use ReferenceOptions to target a specific database instance:
firebase.database.onValueCreated(
ref: 'messages/{messageId}',
options: const ReferenceOptions(instance: 'my-project-default-rtdb'),
(event) async {
print('Instance: ${event.instance}');
},
);Respond to changes in Cloud Storage objects. The bucket parameter specifies which storage bucket to watch.
| Function | Triggers when |
|---|---|
onObjectFinalized |
Object is created or overwritten |
onObjectDeleted |
Object is permanently deleted |
onObjectArchived |
Object is archived (versioned buckets) |
onObjectMetadataUpdated |
Object metadata is updated |
The event data provides full object metadata:
name— Object path within the bucketbucket— Bucket namecontentType— MIME typesize— Content length in bytesstorageClass— Storage class (STANDARD, NEARLINE, COLDLINE, etc.)metadata— User-provided key-value metadatatimeCreated/updated/timeDeleted— Timestampsmd5Hash/crc32c— Checksumsgeneration/metageneration— Versioning info
// Object finalized (created or overwritten)
firebase.storage.onObjectFinalized(
bucket: 'my-bucket',
(event) async {
final data = event.data;
print('Object finalized: ${data?.name}');
print('Content type: ${data?.contentType}');
print('Size: ${data?.size}');
},
);
// Object archived (versioned buckets only)
firebase.storage.onObjectArchived(
bucket: 'my-bucket',
(event) async {
final data = event.data;
print('Object archived: ${data?.name}');
print('Storage class: ${data?.storageClass}');
},
);
// Object deleted
firebase.storage.onObjectDeleted(
bucket: 'my-bucket',
(event) async {
final data = event.data;
print('Object deleted: ${data?.name}');
},
);
// Object metadata updated
firebase.storage.onObjectMetadataUpdated(
bucket: 'my-bucket',
(event) async {
final data = event.data;
print('Metadata updated: ${data?.name}');
print('Metadata: ${data?.metadata}');
},
);Run functions on a recurring schedule using Cloud Scheduler. The schedule parameter accepts standard Unix crontab expressions.
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
Common examples: 0 0 * * * (daily midnight), */5 * * * * (every 5 min), 0 9 * * 1-5 (weekdays 9 AM).
jobName— Cloud Scheduler job name (null if manually invoked)scheduleTime— Scheduled execution time (RFC 3339 string)scheduleDateTime— ParsedDateTimeconvenience getter
// Basic schedule — runs every day at midnight (UTC)
firebase.scheduler.onSchedule(
schedule: '0 0 * * *',
(event) async {
print('Job: ${event.jobName}');
print('Schedule time: ${event.scheduleTime}');
},
);Use ScheduleOptions to set a timezone and configure retry behavior for failed invocations:
firebase.scheduler.onSchedule(
schedule: '0 9 * * 1-5',
options: const ScheduleOptions(
timeZone: TimeZone('America/New_York'),
retryConfig: RetryConfig(
retryCount: RetryCount(3),
maxRetrySeconds: MaxRetrySeconds(60),
minBackoffSeconds: MinBackoffSeconds(5),
maxBackoffSeconds: MaxBackoffSeconds(30),
),
memory: Memory(MemoryOption.mb256),
),
(event) async {
print('Executed at: ${event.scheduleDateTime}');
},
);| RetryConfig field | Description |
|---|---|
retryCount |
Number of retry attempts |
maxRetrySeconds |
Maximum total time for retries |
minBackoffSeconds |
Minimum wait before retry (0-3600) |
maxBackoffSeconds |
Maximum wait before retry (0-3600) |
maxDoublings |
Times to double backoff before going linear |
// App Distribution new tester iOS device
firebase.alerts.appDistribution.onNewTesterIosDevicePublished(
(event) async {
final payload = event.data?.payload;
print('New tester iOS device:');
print(' Tester: ${payload?.testerName} (${payload?.testerEmail})');
print(' Device: ${payload?.testerDeviceModelName}');
print(' Identifier: ${payload?.testerDeviceIdentifier}');
},
);
// Crashlytics fatal issues
firebase.alerts.crashlytics.onNewFatalIssuePublished(
(event) async {
final issue = event.data?.payload.issue;
print('Issue: ${issue?.title}');
print('App: ${event.appId}');
},
);
// Crashlytics ANR (Application Not Responding) issues
firebase.alerts.crashlytics.onNewAnrIssuePublished(
(event) async {
final issue = event.data?.payload.issue;
print('ANR issue: ${issue?.title}');
print('App: ${event.appId}');
},
);
// Crashlytics regression alerts
firebase.alerts.crashlytics.onRegressionAlertPublished(
(event) async {
final payload = event.data?.payload;
print('Regression: ${payload?.type}');
print('Issue: ${payload?.issue.title}');
print('Resolved: ${payload?.resolveTime}');
},
);
// Crashlytics non-fatal issues
firebase.alerts.crashlytics.onNewNonfatalIssuePublished(
(event) async {
final issue = event.data?.payload.issue;
print('Non-fatal issue: ${issue?.title}');
print('App: ${event.appId}');
},
);
// Crashlytics stability digest
firebase.alerts.crashlytics.onStabilityDigestPublished(
(event) async {
final payload = event.data?.payload;
print('Stability digest: ${payload?.digestDate}');
print('Trending issues: ${payload?.trendingIssues.length ?? 0}');
},
);
// Crashlytics velocity alerts
firebase.alerts.crashlytics.onVelocityAlertPublished(
(event) async {
final payload = event.data?.payload;
print('Velocity alert: ${payload?.issue.title}');
print('Crash count: ${payload?.crashCount}');
print('Percentage: ${payload?.crashPercentage}%');
print('First version: ${payload?.firstVersion}');
},
);
// Billing plan updates
firebase.alerts.billing.onPlanUpdatePublished(
(event) async {
final payload = event.data?.payload;
print('New Plan: ${payload?.billingPlan}');
print('Updated By: ${payload?.principalEmail}');
},
);
// Billing automated plan updates
firebase.alerts.billing.onPlanAutomatedUpdatePublished(
(event) async {
final payload = event.data?.payload;
print('Automated plan update:');
print(' Plan: ${payload?.billingPlan}');
print(' Type: ${payload?.notificationType}');
},
);
// Performance threshold alerts
firebase.alerts.performance.onThresholdAlertPublished(
options: const AlertOptions(appId: '1:123456789:ios:abcdef'),
(event) async {
final payload = event.data?.payload;
print('Metric: ${payload?.metricType}');
print('Threshold: ${payload?.thresholdValue}');
print('Actual: ${payload?.violationValue}');
},
);
// App Distribution in-app feedback
firebase.alerts.appDistribution.onInAppFeedbackPublished(
(event) async {
final payload = event.data?.payload;
print('In-app feedback:');
print(' Tester: ${payload?.testerEmail}');
print(' App version: ${payload?.appVersion}');
print(' Text: ${payload?.text}');
print(' Console: ${payload?.feedbackConsoleUri}');
},
);// Custom event (default Firebase channel)
firebase.eventarc.onCustomEventPublished(
eventType: 'com.example.myevent',
(event) async {
print('Event: ${event.type}');
print('Source: ${event.source}');
print('Data: ${event.data}');
},
);
// With channel and filters
firebase.eventarc.onCustomEventPublished(
eventType: 'com.example.filtered',
options: const EventarcTriggerOptions(
channel: 'my-channel',
filters: {'category': 'important'},
),
(event) async {
print('Event: ${event.type}');
print('Data: ${event.data}');
},
);// Before user created
firebase.identity.beforeUserCreated(
options: const BlockingOptions(idToken: true, accessToken: true),
(AuthBlockingEvent event) async {
final user = event.data;
// Block certain email domains
if (user?.email?.endsWith('@blocked.com') ?? false) {
throw PermissionDeniedError('Email domain not allowed');
}
// Set custom claims
if (user?.email?.endsWith('@admin.com') ?? false) {
return const BeforeCreateResponse(
customClaims: {'admin': true},
);
}
return null;
},
);
// Before user signed in
firebase.identity.beforeUserSignedIn(
options: const BlockingOptions(idToken: true),
(AuthBlockingEvent event) async {
return BeforeSignInResponse(
sessionClaims: {
'lastLogin': DateTime.now().toIso8601String(),
'signInIp': event.ipAddress,
},
);
},
);Note:
beforeEmailSentandbeforeSmsSentare also available but cannot be tested with the Firebase Auth emulator (emulator only supportsbeforeUserCreatedandbeforeUserSignedIn). They work in production deployments.
Trigger a function when Firebase Remote Config is updated.
firebase.remoteConfig.onConfigUpdated((event) async {
final data = event.data;
print('Remote Config updated:');
print(' Version: ${data?.versionNumber}');
print(' Description: ${data?.description}');
print(' Update Origin: ${data?.updateOrigin.value}');
print(' Update Type: ${data?.updateType.value}');
print(' Updated By: ${data?.updateUser.email}');
});Trigger a function when a Firebase Test Lab test matrix completes.
firebase.testLab.onTestMatrixCompleted((event) async {
final data = event.data;
print('Test matrix completed:');
print(' Matrix ID: ${data?.testMatrixId}');
print(' State: ${data?.state.value}');
print(' Outcome: ${data?.outcomeSummary.value}');
print(' Client: ${data?.clientInfo.client}');
print(' Results URI: ${data?.resultStorage.resultsUri}');
});final welcomeMessage = defineString(
'WELCOME_MESSAGE',
ParamOptions(
defaultValue: 'Hello from Dart!',
label: 'Welcome Message',
description: 'The greeting message returned by the function',
),
);
final minInstances = defineInt(
'MIN_INSTANCES',
ParamOptions(defaultValue: 0),
);
final isProduction = defineBoolean(
'IS_PRODUCTION',
ParamOptions(defaultValue: false),
);firebase.https.onRequest(
name: 'hello',
(request) async {
return Response.ok(welcomeMessage.value());
},
);firebase.https.onRequest(
name: 'configured',
options: HttpsOptions(
minInstances: DeployOption.param(minInstances),
),
handler,
);firebase.https.onRequest(
name: 'api',
options: HttpsOptions(
// 2GB in production, 512MB in development
memory: Memory.expression(isProduction.thenElse(2048, 512)),
),
(request) async {
final env = isProduction.value() ? 'production' : 'development';
return Response.ok('Running in $env mode');
},
);Your firebase.json must specify the Dart runtime:
{
"functions": [
{
"source": ".",
"codebase": "default",
"runtime": "dart3"
}
],
"emulators": {
"functions": { "port": 5001 },
"firestore": { "port": 8080 },
"database": { "port": 9000 },
"auth": { "port": 9099 },
"pubsub": { "port": 8085 },
"ui": { "enabled": true, "port": 4000 }
}
}firebase emulators:startdart run build_runner buildRun all tests:
dart testRun specific test suites:
# Unit tests only
dart test --exclude-tags=snapshot,integration
# Builder tests
dart run build_runner build --delete-conflicting-outputs
dart test test/builder/
# Snapshot tests (compare with Node.js SDK)
dart test test/snapshots/See Testing Guide for more details.
Apache 2.0