Lightweight realtime streaming clients for Firebase Realtime Database (RTDB) and Firestore in Dart/Flutter, without the full Firebase SDK overhead.
| Approach | Bundle Size | Dependencies |
|---|---|---|
| Full Firebase SDK | ~2-5MB | Firebase Core + Auth + RTDB/Firestore |
| Firebase Realtime Toolkit | ~500KB | http, grpc, protobuf, googleapis_auth |
Perfect for:
- Flutter apps - RTDB realtime streaming with user authentication
- Dart CLI tools - Admin scripts with service account access
- Backend services - Server-side Dart applications with Firestore access
- Reducing bundle size - When full Firebase SDK is overkill
- RTDB Realtime Streaming - Subscribe to Firebase Realtime Database paths via REST SSE
- Firestore Document Listening - Subscribe to Firestore document changes via gRPC
- Generic SSE Client - Use with any Server-Sent Events endpoint
- Cross-Platform - Works on Dart VM, Flutter mobile, desktop, and web (RTDB only)
- Minimal Dependencies - No Firebase SDK required
Add to your pubspec.yaml:
dependencies:
firebase_realtime_toolkit: ^1.0.0Or install via command line:
dart pub add firebase_realtime_toolkitimport 'package:firebase_realtime_toolkit/firebase_realtime_toolkit.dart';
void main() async {
final client = RtdbSseClient(
Uri.parse('https://your-project-id-default-rtdb.firebaseio.com/'),
);
final subscription = client.listen('/path/to/data').listen((event) {
print('Event: ${event.event}');
print('Data: ${event.data}');
});
// When done
await subscription.cancel();
}Listen to Firestore documents without service accounts using Firebase Authentication:
import 'package:firebase_realtime_toolkit/firebase_realtime_toolkit.dart';
void main() async {
// Sign in anonymously (or with email/password)
final tokenProvider = await IdTokenProvider.signInAnonymously(
apiKey: 'your-firebase-web-api-key', // From Firebase Console
projectId: 'your-project-id',
);
final client = FirestoreListenClient(
projectId: 'your-project-id',
tokenProvider: tokenProvider,
);
// Listen to documents - no service account needed!
final subscription = client.listenDocument('public/status').listen((response) {
if (response.hasDocumentChange()) {
print('Document changed: ${response.documentChange.document.fields}');
}
});
// When done
await subscription.cancel();
await client.close();
tokenProvider.close();
}Prerequisites:
- Enable Anonymous Authentication in Firebase Console
- Configure Firestore Security Rules:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /public/{document=**} { allow read: if request.auth != null; } } }
⚠️ SECURITY WARNING - SERVER-SIDE ONLYThe
ServiceAccountTokenProvideris designed for server-side applications only (backend services, CLI tools, CI/CD pipelines).NEVER use service accounts in client applications (Flutter mobile/web/desktop apps). Service accounts contain private keys that grant admin-level access to your entire Firebase project.
import 'package:firebase_realtime_toolkit/firebase_realtime_toolkit.dart';
// ⚠️ SERVER-SIDE ONLY - DO NOT use in Flutter apps
void main() async {
final tokenProvider = ServiceAccountTokenProvider(
'/path/to/service-account.json',
);
final client = FirestoreListenClient(
projectId: 'your-project-id',
tokenProvider: tokenProvider,
);
final subscription = client.listenDocument('collection/docId').listen((response) {
if (response.hasDocumentChange()) {
print('Document changed: ${response.documentChange.document.fields}');
}
});
// When done
await subscription.cancel();
await client.close();
}import 'package:firebase_realtime_toolkit/firebase_realtime_toolkit.dart';
void main() async {
final client = SseClient();
final subscription = client.listen(
Uri.parse('https://example.com/events'),
headers: {'Authorization': 'Bearer token'}, // IO platforms only
).listen((event) {
print('${event.event}: ${event.data}');
});
await subscription.cancel();
}Client for Firebase Realtime Database streaming via REST SSE.
class RtdbSseClient {
RtdbSseClient(Uri baseUri);
Stream<RtdbSseEvent> listen(
String path, {
String? authToken,
Map<String, String>? queryParameters,
});
}Parameters:
baseUri- Firebase RTDB URL (e.g.,https://project-id.firebaseio.com/)path- Database path to listen to (e.g.,/users/123)authToken- Optional Firebase Auth token for authenticated requestsqueryParameters- Optional query parameters (orderBy, limitToFirst, etc.)
Event Types:
| Event | Description |
|---|---|
put |
Initial data load or data set at path |
patch |
Partial update to data |
keep-alive |
Heartbeat (data is null) |
auth_revoked |
Authentication token expired |
cancel |
Listener cancelled or permission denied |
class RtdbSseEvent {
final String event; // Event type
final dynamic data; // Parsed JSON data
final String rawData; // Raw data string
}Client for Firestore document listening via gRPC.
class FirestoreListenClient {
FirestoreListenClient({
required String projectId,
String databaseId = '(default)',
required AccessTokenProvider tokenProvider,
ClientChannel? channel,
});
Stream<ListenResponse> listenDocument(
String documentPath, {
int targetId = 1,
});
Future<void> close();
}Parameters:
projectId- Google Cloud project IDdatabaseId- Firestore database ID (usually(default))tokenProvider- Token provider for authenticationdocumentPath- Document path (e.g.,collection/docId)
Abstract interface for providing authentication tokens.
abstract class AccessTokenProvider {
Future<AccessToken> getAccessToken();
}Provides Firebase ID tokens for client-side authentication using Firebase Auth REST API.
// Sign in anonymously
final provider = await IdTokenProvider.signInAnonymously(
apiKey: 'your-firebase-web-api-key',
projectId: 'your-project-id',
);
// Sign in with email/password
final provider = await IdTokenProvider.signInWithEmailPassword(
apiKey: 'your-firebase-web-api-key',
projectId: 'your-project-id',
email: 'user@example.com',
password: 'password123',
);
// Sign in with custom token (from admin SDK)
final provider = await IdTokenProvider.signInWithCustomToken(
apiKey: 'your-firebase-web-api-key',
projectId: 'your-project-id',
customToken: customTokenFromServer,
);
// Restore from saved session
final provider = await IdTokenProvider.fromSavedTokens(
apiKey: apiKey,
projectId: projectId,
idToken: savedIdToken,
refreshToken: savedRefreshToken,
userId: savedUserId,
);Features:
- Automatic token refresh (tokens expire after 1 hour)
- Save/restore session state
- Works with Firestore security rules
For server-side applications with service account credentials.
class ServiceAccountTokenProvider implements AccessTokenProvider {
ServiceAccountTokenProvider(
String serviceAccountJsonPath, {
List<String> scopes = FirestoreClient.oauthScopes,
});
}Generic SSE client for any Server-Sent Events endpoint.
class SseClient {
Stream<SseEvent> listen(
Uri uri, {
Map<String, String>? headers, // IO platforms only
});
}class SseEvent {
final String event; // Event type
final dynamic data; // Parsed JSON data (or raw string)
final String rawData; // Raw data string
}| Feature | Safe for Client Apps? | Safe for Server Apps? |
|---|---|---|
| RTDB with user auth token | ✅ YES | ✅ YES |
| Generic SSE with user token | ✅ YES | ✅ YES |
| Firestore with IdTokenProvider | ✅ YES | ✅ YES |
| Firestore with service account | ❌ NEVER | ✅ YES |
Why service accounts are dangerous in client apps:
- Service accounts contain private keys with admin access
- Anyone can decompile/extract credentials from your app
- Compromised credentials = complete project access
- Cannot be revoked without replacing everywhere
Safe Alternatives for Client Apps:
// ✅ SAFE: Firestore with IdTokenProvider (anonymous auth)
final tokenProvider = await IdTokenProvider.signInAnonymously(
apiKey: 'your-firebase-web-api-key',
projectId: 'your-project-id',
);
final client = FirestoreListenClient(
projectId: 'your-project-id',
tokenProvider: tokenProvider,
);
final stream = client.listenDocument('public/status');
// ✅ SAFE: RTDB with Firebase Auth token
final client = RtdbSseClient(baseUri);
final stream = client.listen(
'/users/$userId/data',
authToken: firebaseUserIdToken, // From Firebase Auth SDK
);
// ✅ SAFE: Custom SSE endpoint with user authentication
final client = SseClient();
final stream = client.listen(
Uri.parse('https://your-backend.com/stream'),
headers: {'Authorization': 'Bearer $userToken'},
);Server-Side Only:
// ✅ SAFE: Server-side Dart application
final tokenProvider = ServiceAccountTokenProvider(
'/path/to/service-account.json',
);
final client = FirestoreListenClient(
projectId: 'your-project-id',
tokenProvider: tokenProvider,
);| Platform | RTDB SSE | Generic SSE | Firestore Listen |
|---|---|---|---|
| Dart VM (CLI) | ✅ | ✅ | ✅ |
| Flutter Android | ✅ | ✅ | ✅ (with IdTokenProvider) |
| Flutter iOS | ✅ | ✅ | ✅ (with IdTokenProvider) |
| Flutter macOS | ✅ | ✅ | ✅ (with IdTokenProvider) |
| Flutter Windows | ✅ | ✅ | ✅ (with IdTokenProvider) |
| Flutter Linux | ✅ | ✅ | ✅ (with IdTokenProvider) |
| Flutter Web | ✅ | ✅* | ❌ (gRPC not supported) |
* Web SSE uses browser's EventSource API and cannot use custom headers.
** Firestore on Web requires the official Firebase SDK due to gRPC limitations in browsers.
The package includes command-line tools for testing and admin tasks.
Test RTDB streaming with user authentication:
dart run firebase_realtime_toolkit:rtdb_sse \
--base https://project-id-default-rtdb.firebaseio.com/ \
--path /playground/status \
--auth YOUR_AUTH_TOKEN \
--duration 60Client-side tool - uses Firebase Auth (no service account needed):
dart run firebase_realtime_toolkit:firestore_listen_anonymous \
--api-key YOUR_FIREBASE_WEB_API_KEY \
--project my-project-id \
--document public/statusPrerequisites:
- Enable Anonymous Authentication in Firebase Console
- Configure Firestore Security Rules to allow anonymous access
Admin tool - requires service account (server-side only):
dart run firebase_realtime_toolkit:firestore_listen \
--service-account /path/to/service-account.json \
--project my-project-id \
--document collection/docId \
--duration 30Admin tool - requires service account (server-side only):
dart run firebase_realtime_toolkit:firestore_write \
--service-account /path/to/service-account.json \
--project my-project-id \
--document collection/docId \
--field status \
--value "updated"// With Firebase Auth token
final client = RtdbSseClient(baseUri);
final stream = client.listen(
'/private/data',
authToken: userIdToken, // From Firebase Auth
);final channel = ClientChannel(
'firestore.googleapis.com',
port: 443,
options: ChannelOptions(
credentials: ChannelCredentials.secure(),
connectionTimeout: Duration(seconds: 30),
),
);
final client = FirestoreListenClient(
projectId: 'my-project',
tokenProvider: tokenProvider,
channel: channel,
);client.listen('/path').listen(
(event) {
// Handle event
},
onError: (error) {
if (error is HttpException) {
print('HTTP error: ${error.message}');
} else if (error is GrpcError) {
print('gRPC error: ${error.code} - ${error.message}');
}
},
onDone: () {
print('Stream closed');
},
);| Feature | Firebase Realtime Toolkit | Official Firebase SDK |
|---|---|---|
| Bundle Size | ~500KB | ~2-5MB |
| Realtime Streaming | ✅ | ✅ |
| Client-Side Firestore | ✅ (with IdTokenProvider) | ✅ |
| Offline Support | ❌ | ✅ |
| Local Caching | ❌ | ✅ |
| Transactions | ❌ | ✅ |
| Complex Queries | ❌ | ✅ |
| Multi-path Listeners | ❌ | ✅ |
| Auto-reconnect | Partial | ✅ |
| Web Support | RTDB only | ✅ |
Use this toolkit when:
- You need lightweight Firestore realtime listening with minimal bundle size
- You only need RTDB realtime streaming in client apps
- Bundle size is critical
- Building server-side Dart applications with Firestore
- Building CLI tools for Firebase admin tasks
- Want to avoid Firebase SDK initialization overhead
Use official Firebase SDK when:
- You need offline support
- You need complex queries
- You need transactions
- You need Firestore on web
- You need the full Firebase feature set
- Architecture Guide - Detailed technical architecture
- Usage Example - Real-world Storyboard case study with REST vs SSE comparison
- Firebase RTDB REST API
- Firestore gRPC API
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
MIT License - see LICENSE for details.
Developed by Codelessly