@@ -7,18 +7,26 @@ Write Firebase Cloud Functions in Dart with full type safety and performance.
77
88## Status: Alpha (v0.1.0)
99
10- This package is in active development. Phase 1 includes:
11- - ✅ HTTPS triggers (onRequest, onCall, onCallWithData)
12- - ✅ Pub/Sub triggers (onMessagePublished)
13- - ✅ CloudEvent foundation
14- - ✅ Build-time code generation
10+ This package provides a complete Dart implementation of Firebase Cloud Functions with support for:
11+
12+ | Trigger Type | Status | Functions |
13+ | -------------| --------| -----------|
14+ | ** HTTPS** | ✅ Complete | ` onRequest ` , ` onCall ` , ` onCallWithData ` |
15+ | ** Pub/Sub** | ✅ Complete | ` onMessagePublished ` |
16+ | ** Firestore** | ✅ Complete | ` onDocumentCreated ` , ` onDocumentUpdated ` , ` onDocumentDeleted ` , ` onDocumentWritten ` |
17+ | ** Realtime Database** | ✅ Complete | ` onValueCreated ` , ` onValueUpdated ` , ` onValueDeleted ` , ` onValueWritten ` |
18+ | ** Firebase Alerts** | ✅ Complete | Crashlytics, Billing, Performance alerts |
19+ | ** Identity Platform** | ✅ Complete | ` beforeUserCreated ` , ` beforeUserSignedIn ` (+ ` beforeEmailSent ` , ` beforeSmsSent ` * ) |
1520
1621## Features
1722
18- - ** Type-safe** : Leverage Dart's strong type system
23+ - ** Type-safe** : Leverage Dart's strong type system with typed callable functions and CloudEvents
1924- ** Fast** : Compiled Dart code with efficient Shelf HTTP server
20- - ** Familiar API** : Similar to Firebase Functions Node.js SDK
21- - ** Testing** : Built with testing in mind
25+ - ** Familiar API** : Similar to Firebase Functions Node.js SDK v2
26+ - ** Streaming** : Server-Sent Events (SSE) support for callable functions
27+ - ** Parameterized** : Deploy-time configuration with ` defineString ` , ` defineInt ` , ` defineBoolean `
28+ - ** Conditional Config** : CEL expressions for environment-based options
29+ - ** Error Handling** : Built-in typed error classes matching the Node.js SDK
2230- ** Hot Reload** : Fast development with build_runner watch
2331
2432## Installation
@@ -33,45 +41,349 @@ dependencies:
3341
3442## Quick Start
3543
36- ### HTTPS Function
37-
3844` ` ` dart
3945import 'package:firebase_functions/firebase_functions.dart';
40- import 'package:shelf/shelf.dart';
4146
4247void main(List<String> args) {
4348 fireUp(args, (firebase) {
44- firebase.https.onRequest(
45- name : ' hello' ,
46- (request) async {
47- return Response.ok('Hello from Dart!');
48- },
49- );
49+ // Register your functions here
5050 });
5151}
5252```
5353
54- ### Callable Function
54+ ## HTTPS Functions
55+
56+ ### onRequest - Raw HTTP Handler
57+
58+ ``` dart
59+ firebase.https.onRequest(
60+ name: 'hello',
61+ (request) async {
62+ return Response.ok('Hello from Dart!');
63+ },
64+ );
65+ ```
66+
67+ ### onCall - Untyped Callable
5568
5669``` dart
5770firebase.https.onCall(
5871 name: 'greet',
5972 (request, response) async {
60- final name = request.data['name'] as String;
61- return CallableResult({'message': 'Hello $name!'});
73+ final data = request.data as Map<String, dynamic>?;
74+ final name = data?['name'] ?? 'World';
75+ return CallableResult({'message': 'Hello, $name!'});
76+ },
77+ );
78+ ```
79+
80+ ### onCallWithData - Type-safe Callable
81+
82+ ``` dart
83+ firebase.https.onCallWithData<GreetRequest, GreetResponse>(
84+ name: 'greetTyped',
85+ fromJson: GreetRequest.fromJson,
86+ (request, response) async {
87+ return GreetResponse(message: 'Hello, ${request.data.name}!');
88+ },
89+ );
90+ ```
91+
92+ ### Streaming Support
93+
94+ ``` dart
95+ firebase.https.onCall(
96+ name: 'countdown',
97+ options: const CallableOptions(
98+ heartBeatIntervalSeconds: HeartBeatIntervalSeconds(5),
99+ ),
100+ (request, response) async {
101+ if (request.acceptsStreaming) {
102+ for (var i = 10; i >= 0; i--) {
103+ await response.sendChunk({'count': i});
104+ await Future.delayed(Duration(milliseconds: 100));
105+ }
106+ }
107+ return CallableResult({'message': 'Countdown complete!'});
108+ },
109+ );
110+ ```
111+
112+ ### Error Handling
113+
114+ ``` dart
115+ firebase.https.onCall(
116+ name: 'divide',
117+ (request, response) async {
118+ final data = request.data as Map<String, dynamic>?;
119+ final a = data?['a'] as num?;
120+ final b = data?['b'] as num?;
121+
122+ if (a == null || b == null) {
123+ throw InvalidArgumentError('Both "a" and "b" are required');
124+ }
125+ if (b == 0) {
126+ throw FailedPreconditionError('Cannot divide by zero');
127+ }
128+
129+ return CallableResult({'result': a / b});
62130 },
63131);
64132```
65133
66- ### Pub/Sub Trigger
134+ Available error types: ` InvalidArgumentError ` , ` FailedPreconditionError ` , ` NotFoundError ` , ` AlreadyExistsError ` , ` PermissionDeniedError ` , ` ResourceExhaustedError ` , ` UnauthenticatedError ` , ` UnavailableError ` , ` InternalError ` , ` DeadlineExceededError ` , ` CancelledError ` .
135+
136+ ## Pub/Sub Triggers
67137
68138``` dart
69139firebase.pubsub.onMessagePublished(
70140 topic: 'my-topic',
71141 (event) async {
72142 final message = event.data;
73- print('Received: ${message.textData}');
74- print('Attributes: ${message.attributes}');
143+ print('ID: ${message?.messageId}');
144+ print('Data: ${message?.textData}');
145+ print('Attributes: ${message?.attributes}');
146+ },
147+ );
148+ ```
149+
150+ ## Firestore Triggers
151+
152+ ``` dart
153+ // Document created
154+ firebase.firestore.onDocumentCreated(
155+ document: 'users/{userId}',
156+ (event) async {
157+ final data = event.data?.data();
158+ print('Created: users/${event.params['userId']}');
159+ print('Name: ${data?['name']}');
160+ },
161+ );
162+
163+ // Document updated
164+ firebase.firestore.onDocumentUpdated(
165+ document: 'users/{userId}',
166+ (event) async {
167+ final before = event.data?.before?.data();
168+ final after = event.data?.after?.data();
169+ print('Before: $before');
170+ print('After: $after');
171+ },
172+ );
173+
174+ // Document deleted
175+ firebase.firestore.onDocumentDeleted(
176+ document: 'users/{userId}',
177+ (event) async {
178+ final data = event.data?.data();
179+ print('Deleted data: $data');
180+ },
181+ );
182+
183+ // All write operations
184+ firebase.firestore.onDocumentWritten(
185+ document: 'users/{userId}',
186+ (event) async {
187+ final before = event.data?.before?.data();
188+ final after = event.data?.after?.data();
189+ // Determine operation type
190+ if (before == null && after != null) print('CREATE');
191+ if (before != null && after != null) print('UPDATE');
192+ if (before != null && after == null) print('DELETE');
193+ },
194+ );
195+
196+ // Nested collections
197+ firebase.firestore.onDocumentCreated(
198+ document: 'posts/{postId}/comments/{commentId}',
199+ (event) async {
200+ print('Post: ${event.params['postId']}');
201+ print('Comment: ${event.params['commentId']}');
202+ },
203+ );
204+ ```
205+
206+ ## Realtime Database Triggers
207+
208+ ``` dart
209+ // Value created
210+ firebase.database.onValueCreated(
211+ ref: 'messages/{messageId}',
212+ (event) async {
213+ final data = event.data?.val();
214+ print('Created: ${event.params['messageId']}');
215+ print('Data: $data');
216+ print('Instance: ${event.instance}');
217+ },
218+ );
219+
220+ // Value updated
221+ firebase.database.onValueUpdated(
222+ ref: 'messages/{messageId}',
223+ (event) async {
224+ final before = event.data?.before?.val();
225+ final after = event.data?.after?.val();
226+ print('Before: $before');
227+ print('After: $after');
228+ },
229+ );
230+
231+ // Value deleted
232+ firebase.database.onValueDeleted(
233+ ref: 'messages/{messageId}',
234+ (event) async {
235+ final data = event.data?.val();
236+ print('Deleted: $data');
237+ },
238+ );
239+
240+ // All write operations
241+ firebase.database.onValueWritten(
242+ ref: 'users/{userId}/status',
243+ (event) async {
244+ final before = event.data?.before;
245+ final after = event.data?.after;
246+ if (before == null || !before.exists()) print('CREATE');
247+ else if (after == null || !after.exists()) print('DELETE');
248+ else print('UPDATE');
249+ },
250+ );
251+ ```
252+
253+ ## Firebase Alerts
254+
255+ ``` dart
256+ // Crashlytics fatal issues
257+ firebase.alerts.crashlytics.onNewFatalIssuePublished(
258+ (event) async {
259+ final issue = event.data?.payload.issue;
260+ print('Issue: ${issue?.title}');
261+ print('App: ${event.appId}');
262+ },
263+ );
264+
265+ // Billing plan updates
266+ firebase.alerts.billing.onPlanUpdatePublished(
267+ (event) async {
268+ final payload = event.data?.payload;
269+ print('New Plan: ${payload?.billingPlan}');
270+ print('Updated By: ${payload?.principalEmail}');
271+ },
272+ );
273+
274+ // Performance threshold alerts
275+ firebase.alerts.performance.onThresholdAlertPublished(
276+ options: const AlertOptions(appId: '1:123456789:ios:abcdef'),
277+ (event) async {
278+ final payload = event.data?.payload;
279+ print('Metric: ${payload?.metricType}');
280+ print('Threshold: ${payload?.thresholdValue}');
281+ print('Actual: ${payload?.violationValue}');
282+ },
283+ );
284+ ```
285+
286+ ## Identity Platform (Auth Blocking)
287+
288+ ``` dart
289+ // Before user created
290+ firebase.identity.beforeUserCreated(
291+ options: const BlockingOptions(idToken: true, accessToken: true),
292+ (AuthBlockingEvent event) async {
293+ final user = event.data;
294+
295+ // Block certain email domains
296+ if (user?.email?.endsWith('@blocked.com') ?? false) {
297+ throw PermissionDeniedError('Email domain not allowed');
298+ }
299+
300+ // Set custom claims
301+ if (user?.email?.endsWith('@admin.com') ?? false) {
302+ return const BeforeCreateResponse(
303+ customClaims: {'admin': true},
304+ );
305+ }
306+
307+ return null;
308+ },
309+ );
310+
311+ // Before user signed in
312+ firebase.identity.beforeUserSignedIn(
313+ options: const BlockingOptions(idToken: true),
314+ (AuthBlockingEvent event) async {
315+ return BeforeSignInResponse(
316+ sessionClaims: {
317+ 'lastLogin': DateTime.now().toIso8601String(),
318+ 'signInIp': event.ipAddress,
319+ },
320+ );
321+ },
322+ );
323+ ```
324+
325+ > ** Note** : ` beforeEmailSent ` and ` beforeSmsSent ` are also available but cannot be tested with the Firebase Auth emulator (emulator only supports ` beforeUserCreated ` and ` beforeUserSignedIn ` ). They work in production deployments.
326+
327+ ## Parameters & Configuration
328+
329+ ### Defining Parameters
330+
331+ ``` dart
332+ final welcomeMessage = defineString(
333+ 'WELCOME_MESSAGE',
334+ ParamOptions(
335+ defaultValue: 'Hello from Dart!',
336+ label: 'Welcome Message',
337+ description: 'The greeting message returned by the function',
338+ ),
339+ );
340+
341+ final minInstances = defineInt(
342+ 'MIN_INSTANCES',
343+ ParamOptions(defaultValue: 0),
344+ );
345+
346+ final isProduction = defineBoolean(
347+ 'IS_PRODUCTION',
348+ ParamOptions(defaultValue: false),
349+ );
350+ ```
351+
352+ ### Using Parameters at Runtime
353+
354+ ``` dart
355+ firebase.https.onRequest(
356+ name: 'hello',
357+ (request) async {
358+ return Response.ok(welcomeMessage.value());
359+ },
360+ );
361+ ```
362+
363+ ### Using Parameters in Options (Deploy-time)
364+
365+ ``` dart
366+ firebase.https.onRequest(
367+ name: 'configured',
368+ options: HttpsOptions(
369+ minInstances: DeployOption.param(minInstances),
370+ ),
371+ handler,
372+ );
373+ ```
374+
375+ ### Conditional Configuration
376+
377+ ``` dart
378+ firebase.https.onRequest(
379+ name: 'api',
380+ options: HttpsOptions(
381+ // 2GB in production, 512MB in development
382+ memory: Memory.expression(isProduction.thenElse(2048, 512)),
383+ ),
384+ (request) async {
385+ final env = isProduction.value() ? 'production' : 'development';
386+ return Response.ok('Running in $env mode');
75387 },
76388);
77389```
0 commit comments