firebase_functions 0.5.1
firebase_functions: ^0.5.1 copied to clipboard
Cloud Functions for Dart with support for the Firebase Admin SDK, Cloud Storage and Firestore
Write Firebase Cloud Functions in Dart with full type safety and performance.
Status: Experimental (v0.5.0) #
This package provides a Dart implementation of Firebase Cloud Functions. Only HTTPS triggers are currently supported in production. Other trigger types are experimental and have varying levels of support.
| Trigger Type | Status | Functions |
|---|---|---|
| HTTPS | ✅ Production | onRequest, onCall, onCallWithData (see note) |
| Firestore | ⚠️ Emulator only | onDocumentCreated, onDocumentUpdated, onDocumentDeleted, onDocumentWritten, onDocumentCreatedWithAuthContext, onDocumentUpdatedWithAuthContext, onDocumentDeletedWithAuthContext, onDocumentWrittenWithAuthContext |
| Realtime Database | ⚠️ Emulator only | onValueCreated, onValueUpdated, onValueDeleted, onValueWritten |
| Storage | ⚠️ Emulator only | onObjectFinalized, onObjectArchived, onObjectDeleted, onObjectMetadataUpdated |
| Pub/Sub | 🚧 Experimental | onMessagePublished |
| Scheduler | 🚧 Experimental | onSchedule |
| Firebase Alerts | 🚧 Experimental | onAlertPublished and sub-namespace triggers |
| Eventarc | 🚧 Experimental | onCustomEventPublished |
| Identity Platform | 🚧 Experimental | beforeUserCreated, beforeUserSignedIn (+ beforeEmailSent, beforeSmsSent) |
| Remote Config | 🚧 Experimental | onConfigUpdated |
| Test Lab | 🚧 Experimental | onTestMatrixCompleted |
| Task Queues | 🚧 Experimental | onTaskDispatched |
Legend: ✅ Production — works in production and emulator. ⚠️ Emulator only — works with the Firebase emulator but not yet in production. 🚧 Experimental — implemented but not currently supported by the emulator or production; APIs may change.
Note
When invoking functions defined with onCall and onCallWithData from a
client SDK, you must use the function's HTTPS URL. The standard name-based
lookup is not supported for functions written in Dart. For example, in the
Flutter cloud_functions package, use httpsCallableFromUrl.
Table of Contents #
Features #
- 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
Quick Start #
import 'package:firebase_functions/firebase_functions.dart';
void main(List<String> args) {
fireUp(args, (firebase) {
// Register your functions here
});
}
HTTPS Functions #
onRequest - Raw HTTP Handler #
firebase.https.onRequest(
name: 'hello',
(request) async {
return Response.ok('Hello from Dart!');
},
);
onCall - Untyped Callable #
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!'});
},
);
onCallWithData - Type-safe Callable #
firebase.https.onCallWithData<GreetRequest, GreetResponse>(
name: 'greetTyped',
fromJson: GreetRequest.fromJson,
(request, response) async {
return GreetResponse(message: 'Hello, ${request.data.name}!');
},
);
Streaming Support #
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!'});
},
);
Error Handling #
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.
Pub/Sub Triggers #
firebase.pubsub.onMessagePublished(
topic: 'my-topic',
(event) async {
final message = event.data;
print('ID: ${message?.messageId}');
print('Data: ${message?.textData}');
print('Attributes: ${message?.attributes}');
},
);
Firestore Triggers #
// 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');
},
);
Realtime Database Triggers #
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) |
DataSnapshot API #
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');
},
);
Database Instance Targeting #
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}');
},
);
Storage Triggers #
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 |
StorageObjectData Properties #
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}');
},
);
Scheduler Triggers #
Run functions on a recurring schedule using Cloud Scheduler. The schedule parameter accepts standard Unix crontab expressions.
Cron Syntax #
┌───────────── 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).
ScheduledEvent Properties #
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}');
},
);
Timezone and Retry Configuration #
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 |
Firebase Alerts #
// 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}');
},
);
Eventarc #
// 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}');
},
);
Identity Platform (Auth Blocking) #
// 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.
Remote Config #
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}');
});
Test Lab #
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}');
});
Parameters & Configuration #
Defining Parameters #
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),
);
Using Parameters at Runtime #
firebase.https.onRequest(
name: 'hello',
(request) async {
return Response.ok(welcomeMessage.value());
},
);
Using Parameters in Options (Deploy-time) #
firebase.https.onRequest(
name: 'configured',
options: HttpsOptions(
minInstances: DeployOption.param(minInstances),
),
handler,
);
Conditional Configuration #
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');
},
);
Project Configuration #
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 }
}
}
Development #
Running the Emulator #
firebase emulators:start
Building #
dart run build_runner build
Testing #
Run all tests:
dart test
Run 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.