Stream Authentication Using Flutter, Firebase, and Cloud Functions

In this tutorial, you’ll learn how to build serverless authentication for a messaging app using Flutter, Firebase, and Cloud Functions.

Souvik B.
Souvik B.
Published December 2, 2021
Stream Authentication Using Flutter, Firebase, and Cloud Functions

Authentication is a basic necessity when building a messaging app with Stream. It helps secure the messaging environment and also provides a customized experience on a per-user basis.

Stream uses JWT (JSON Web Tokens) to authenticate users. Generally, to generate and provide these authentication tokens to your app, you need to maintain a backend server.

But in this article, you’ll learn how you can easily handle the whole authentication process in a serverless environment using Firebase Authentication and Cloud Functions.

Introduction to Firebase Authentication and Cloud Functions

Firebase provides pre-configured backend services to help you build serverless applications. Firebase Authentication and Cloud Functions are two of the services provided by Firebase.

Firebase Authentication lets you secure your application without having to maintain any backend infrastructure. It supports the traditional email-password authentication, as well as integration with various identity providers like Google, Apple, Facebook, and GitHub.

Cloud Functions allow you to run backend code on the serverless infrastructure managed by Firebase. You can trigger functions based upon the events of any Firebase service, or you can also define functions that need to be triggered by HTTP requests.

We’ll explore these in detail throughout the article. Let’s get started by creating a new Flutter project.

Create Your Flutter app

You can create a new Flutter project directly using your IDE (like VS Code, IntelliJ, Android Studio), or from your terminal using the following command:

bash
1
flutter create stream_auth_firebase

Note: This tutorial was created using version 2.5.3 of Flutter from the stable channel.

Open the project using your preferred IDE and navigate to the pubspec.yaml file. Add the following dependencies:

yaml
1
dependencies: firebase_core: ^1.10.0 firebase_auth: ^3.2.0 cloud_functions: ^3.1.1 stream_chat_flutter: ^3.2.0

Go to your lib/main.dart file, and replace it with the following code:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); runApp(MyApp()); } class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Stream Auth', theme: ThemeData( primarySwatch: Colors.blue, ), debugShowCheckedModeBanner: false, home: LoginPage(), ); } } class LoginPage extends StatefulWidget { const LoginPage({Key? key}) : super(key: key); _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { Widget build(BuildContext context) { return Container(); } }

Inside the main() method, Firebase is initialized with the Firebase.initializeApp() call. You should call this method before using any other Firebase services, otherwise, it may result in an error.

The LoginPage is empty for now (we will work on it in a while to build the sign-in and sign-up interface). But before that, let’s complete the Firebase setup.

Stream Setup

You will need to create a Stream app to access chat messaging features. If you don’t already have a Stream account, you can start your free trial for Stream’s Chat Messaging.

Working on a personal project? You can register for a Stream Maker Account and access Stream Chat for free indefinitely.

From the Stream dashboard:

  1. Click Create App.
  2. Enter your app name.
  3. Select a server location.
  4. Set your Environment as Development.
  5. Click Create App.
Create a new app in Stream dashboard

This will create a new Stream App. You can navigate to the app from the Stream dashboard:

Stream dashboard

Two important things that you’ll need in order to access Stream from your app and cloud functions:

  • API Key: App identifier, safe to share publicly
  • Secret: Helps generate user tokens, should be kept private

Create a new Firebase Project

You have to create a new Firebase project to integrate it with your Stream Flutter app and access the services. Follow the steps below to create a new Firebase project:

  1. Go to the Firebase console.
  2. Click Add project.
Create a Firebase project
  1. Enter a Project name and click Continue.
Name your Firebase project
  1. Next, you will be asked whether you want to enable Google Analytics for the project. You won’t need analytics as this is just a sample project. Click Create project.
Google Analytics prompt

If you want to enable Google Analytics, you can select a Google Analytics account on the next screen: Configure Google Analytics

Once the project is created, Firebase will navigate you to your project’s Firebase dashboard.

StreamAuth app created

Configure Firebase for Android and iOS

After creating your Firebase project, you can integrate Firebase with the Android and iOS platforms. This will allow you to access Firebase services within your app.

Android configuration

  1. From the Firebase dashboard, click on the Android icon.
Getting started with Firebase for Android
  1. Enter the Android package name, an App nickname, and the SHA-1. Click Register app.
Adding Firebase to your Android app
  1. Download the google-services.json file and place it in your android -> app directory. Click Next.
Download the google-services.json file
  1. Follow the steps and add the required code snippets to your project. Click Next.
Add the Firebase SDK
  1. Finally, click Continue to console to return to your Firebase dashboard.
Click "Continue to console"

You have successfully configured Firebase for Android.

iOS configuration

  1. Click Add app and select the iOS icon.
Firebase for iOS
  1. Enter your iOS bundle ID and an App nickname. Click Register app.
Add Firebase to your iOS app
  1. Download the GoogleService-Info.plist file. Click Next.
Download the GoogleService file
  1. Back in your project:

    1. Open the ios folder using Xcode.
    2. Drag and drop the file that you downloaded into the Runner subfolder.
    3. When a dialog box appears, make sure that Runner is selected in the Add to targets box. Click Finish.
  2. You can skip steps three and four, as they are automatically configured by the Flutter Firebase plugin.
Adding the Firebase SDK
  1. Click Continue to console to go back to the Firebase dashboard.
Next steps

You have successfully configured Firebase for the iOS platform as well.

Set Up Firebase Cloud Functions

You should upgrade your Firebase project to the Blaze Plan to access the Cloud Functions service.

You can upgrade your project by going to the Firebase dashboard and clicking on the Modify button on the left menu beside the current plan (every Firebase project uses the Spark Plan by default).

Blaze plan

Once you have the project upgraded, select Functions from the left sidebar and click Get Started.

Firebase Get Started page

A Set up Functions dialog box will open with Install and Deploy steps.

Set up functions - Install

If you don’t have Firebase tools installed on your system, run the Install step:

bash
1
$ npm install -g firebase-tools

Click Continue. In the Deploy step, you will find commands for initializing and deploying functions:

Set up functions - deploy

Follow the steps below to get started writing cloud functions:

  1. Navigate to your Flutter project directory, and initialize the project there using:
bash
1
firebase init
  1. When prompted to select the Firebase features to use, select Functions.
Initialize Firebase in your terminal
  1. In Project Setup, select Use an existing project and choose the Firebase project that you created earlier.
Project Setup screen in terminal
  1. In Functions Setup, choose:
    1. Language: JavaScript
    2. ESLint enable: Yes
    3. Install dependencies: Yes
Project setup in terminal

Once the Firebase initialization process finishes, it will generate a new folder inside your project directory called functions with the following content:

Functions folder

The package.json contains the dependencies for your Cloud Functions. You will need to define the functions inside the index.js file.

Defining Cloud Functions

You need to install the stream-chat Node.js dependency to access the Stream APIs.
Run the following command:

bash
1
npm install stream-chat --save-prod

This command will install the dependency and add it to the package.json file.
Navigate to the functions/index.js file and import the required dependencies:

javascript
1
2
3
const StreamChat = require("stream-chat").StreamChat; const functions = require("firebase-functions"); const admin = require("firebase-admin");

Before using any Firebase services, you will need to initialize the app:

javascript
1
admin.initializeApp();

Create an instance of the Stream client:

javascript
1
2
3
4
const serverClient = StreamChat.getInstance( functions.config().stream.key, functions.config().stream.secret, );

To create the server-client, you will need the Stream Key and Stream Secret

Note: functions.config() helps in passing these as arguments while deploying the functions.

Create a Stream User

When a user registers in the app, a new Stream user needs to be generated using a cloud function trigger. Let’s define a function called createStreamUserAndGetToken:

javascript
1
2
3
4
5
6
7
exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => { if (!context.auth) { // Throw an error message } else { // Create a new user } });

Here, you use context.auth to check if the user is authenticated. In case the user is not authenticated, throw an error:

javascript
1
2
3
4
throw new functions.https.HttpsError("failed-precondition", "The function must be called " + "while authenticated." );
Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

If the user is authenticated, a new Stream user can be generated:

javascript
1
2
3
4
5
6
7
8
9
10
try { await serverClient.upsertUser({ id: context.auth.uid, name: context.auth.token.name, email: context.auth.token.email, image: context.auth.token.image, }); return serverClient.createToken(context.auth.uid); } catch (err) { console.error(

You can create a new Stream user by calling the upsertUser() method on the serverClient object, passing the user data retrieved from the authenticated user.

The most important parameter here is id. To make sure the id is always unique, we use the uid generated by Firebase (uid is a unique identifier for every Firebase authenticated user).

Once the user is created, you can retrieve the token using the createToken() method. You will need this token inside the Flutter app to get access to the user and make requests to the Stream API.

Get Stream User Token

For registered users, you don’t need to generate a new Stream user, just a new token. You can define the following function for the signed-in users:

javascript
1
2
3
4
5
6
7
8
exports.getStreamUserToken = functions.https.onCall((data, context) => { if (!context.auth) { throw new functions.https.HttpsError("failed-precondition", "The function must be called " + "while authenticated."); } else { try { return serverClient.createToken(context.auth.uid); } catch (err) { console.error(

If the user is authenticated, the authenticated user’s uid is used to generate a new token. Otherwise, an error is thrown.

Revoke Stream User token

If a user signs out of Firebase within the app, you should revoke the Stream user token using a cloud function:

javascript
1
2
3
4
5
6
7
8
exports.revokeStreamUserToken = functions.https.onCall((data, context) => { if (!context.auth) { throw new functions.https.HttpsError("failed-precondition", "The function must be called " + "while authenticated."); } else { try { return serverClient.revokeUserToken(context.auth.uid); } catch (err) { console.error(

The revokeUserToken() method is used by passing the authenticated user’s uid. This will revoke the current Stream user token.

Delete Stream User

Finally, we will define a cloud function to delete the Stream user when its respective user is deleted from Firebase:

javascript
1
2
3
exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => { return serverClient.deleteUser(user.uid); });

You won’t need to call this function explicitly, this will automatically trigger when an account is deleted from Firebase.

Deploying Functions to Firebase

You must deploy the functions to Firebase before you can trigger them from the Flutter app.

Before deploying your functions, you need to store the Stream Key and the Stream Secret as environment variables. Use the following command:

bash
1
firebase functions:config:set stream.key="app-key" stream.secret="app-secret"

Replace the app-key and app-secret placeholders with their appropriate values.
Finally, deploy your functions using:

bash
1
firebase deploy --only functions

After successfully deploying your cloud functions, you will be able to see them on the Firebase Functions page.

(You can find this page in your Firebase project dashboard by selecting Functions from the left menu.)

Firebase functions

Integrate Firebase Authentication

Let’s return to the Flutter app to implement Firebase Authentication.

Create a new Dart file called authentication.dart and define a new class called Authentication:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Authentication { FirebaseAuth auth = FirebaseAuth.instance; FirebaseFunctions functions = FirebaseFunctions.instance; static User? firebaseUser; // Custom snackbar widget static SnackBar customSnackBar({required String content}) { return SnackBar( backgroundColor: Colors.black, content: Text( content, style: TextStyle(color: Colors.redAccent, letterSpacing: 0.5), ), ); } // ... }

Firebase Auth and Firebase Functions are instantiated. Also, a variable of type User is defined, inside which the user information is stored after authentication.

The customSnackBar is defined to easily display a custom error during the authentication process.

Register Using Email/Password

Define a method called registerUsingEmailPassword() that accepts a few parameters like name, email, password, and context:

dart
1
2
3
4
5
6
7
8
Future<void> registerUsingEmailPassword({ required String name, required String email, required String password, required BuildContext context, }) async { // Define the user registration process }

You can define a new user registration process like this:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
try { UserCredential userCredential = await auth.createUserWithEmailAndPassword( email: email, password: password, ); firebaseUser = userCredential.user; if (firebaseUser != null) { await firebaseUser!.updateDisplayName(name); await firebaseUser!.reload(); firebaseUser = auth.currentUser; } else { throw ('Firebase user is null'); } final callable = functions.httpsCallable('createStreamUserAndGetToken'); final results = await callable(); String? token = results.data; if (token != null) { print('Stream token retrieved (registered)'); StreamClient.initialize(token, context); } } on FirebaseAuthException catch (e) { // Handle an error caused by Firebase } catch (e) { // In case of any other kind of error print(e); }

In the above code snippet, the createUserWithEmailAndPassword() method is called on the auth object to create a new user and get the credentials.

The authenticated user can be retrieved from the credentials. To set the name of the user, the updateDisplayName() method is used.

Once you have the authenticated user, you can call the createStreamUserAndGetToken cloud function using functions.httpsCallable().

The cloud function returns the generated Stream user token, which is used to initialize Stream in the Flutter app using StreamClient.initialize(). (The StreamClient is a custom class that we’ll define after the Authentication class is complete.)

The Firebase Exceptions can be handled like this:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
try { // ... } on FirebaseAuthException catch (e) { if (e.code == 'weak-password') { print('The password provided is too weak.'); ScaffoldMessenger.of(context).showSnackBar( Authentication.customSnackBar( content: 'The password provided is too weak.', ), ); } else if (e.code == 'email-already-in-use') { print('The account already exists for that email.'); ScaffoldMessenger.of(context).showSnackBar( Authentication.customSnackBar( content: 'The account already exists for that email.', ), ); } } catch (e) { print(e); }

Sign-In

The method for Firebase Sign-In using email/password is similar to the registration process.

But here, you’ll need to use the signInWithEmailAndPassword() method on the auth object to get the user credentials and use the getStreamUserToken cloud function to generate a new token for the user.

Then similarly, use the token to initialize Stream Chat in the app:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Future<void> signInUsingEmailPassword({ required String email, required String password, required BuildContext context, }) async { String? token; try { UserCredential userCredential = await auth.signInWithEmailAndPassword( email: email, password: password, ); firebaseUser = userCredential.user; final callable = functions.httpsCallable('getStreamUserToken'); final results = await callable(); token = results.data; if (token != null) { print('Stream token retrieved (signed in)'); StreamClient.initialize(token, context); } } on FirebaseAuthException catch (e) { if (e.code == 'user-not-found') { print('No user found for that email.'); ScaffoldMessenger.of(context).showSnackBar( Authentication.customSnackBar( content: 'No user found for that email. Please create an account.', ), ); } else if (e.code == 'wrong-password') { print('Wrong password provided.'); ScaffoldMessenger.of(context).showSnackBar( Authentication.customSnackBar( content: 'Wrong password provided.', ), ); } } }

Sign Out

Use this method to sign out of Firebase. While a user tries to sign out of Firebase, you should also revoke the current Stream token of the user by calling the revokeStreamUserToken cloud function.

Once the token is revoked, close the Stream client connection by calling the closeConnection() method before signing out of Firebase:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
Future<void> signOut() async { // Revoke Stream user token final callable = functions.httpsCallable('revokeStreamUserToken'); await callable(); print('Stream user token revoked'); // Close connection StreamClient.client.closeConnection(); // Sign out Firebase await auth.signOut(); print('Firebase signed out'); }

Define Stream Client

Create a new file called stream_client.dart and define the StreamClient class. This class will be responsible for instantiating the StreamChatClient, connecting to the Stream user, and navigating to the Stream Chat page.

To instantiate the client:

dart
1
2
3
4
5
6
class StreamClient { static final client = StreamChatClient( streamKey, logLevel: Level.OFF, ); }

Replace streamKey with your Stream app API Key.

Define the initialize() method:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class StreamClient { // ... static initialize(String token, BuildContext context) async { final authenticatedUser = Authentication.firebaseUser!; await client.connectUser( User( id: authenticatedUser.uid, extraData: { 'name': authenticatedUser.displayName, 'image': authenticatedUser.photoURL, }, ), token, ); final channel = client.channel('messaging', id: 'general'); await channel.watch(); Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => MaterialApp( builder: (context, widget) { return StreamChat( child: widget, client: client, ); }, debugShowCheckedModeBanner: false, home: ChannelListPage(channel), ), ), ); } }

In the above code snippet, you:

  1. Retrieve the Firebase authenticated user.
  2. Connect to the Stream user with the client.connectUser() method.
  3. Set up a channel for messaging.
  4. Navigate to the ChannelListPage containing a list of all channels of your Stream app.

Building the Login and Register Pages

Now that you have defined most of the functionalities, you can start building the interface of the app. There will be two pages responsible for authenticating the user:

  • Login Page: For users who are already registered in the app
  • Register Page: For new users who want to access the app
Register and Login authentication pages

Let’s start building the Login Page UI.

This page will mainly consist of two TextField widgets for taking the email and password as user inputs, and a button for triggering the email/password sign-in process.

Define the LoginPage as a StatefulWidget:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class LoginPage extends StatefulWidget { _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final _authentication = Authentication(); final _loginFormKey = GlobalKey<FormState>(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isSigningIn = false; Widget build(BuildContext context) { return Scaffold(); } }

Here, you instantiate the Authentication class and define some variables needed to build the UI.

Next, define two validators to verify if the email and password are entered in the correct format and that they are not empty:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
_emailValidator(String? email) { if (email == null || email.isEmpty) { return 'Please enter a valid email'; } return null; } _passwordValidator(String? password) { if (password == null || password.isEmpty) { return 'Please enter a password of 6 characters or more'; } return null; }

Now, you can define the UI inside the build() method:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Stream Login'), ), body: Padding( padding: const EdgeInsets.all(16.0), child: Form( key: _loginFormKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ TextFormField( controller: _emailController, validator: (value) => _emailValidator(value), decoration: InputDecoration(hintText: 'Email'), ), SizedBox(height: 16.0), TextFormField( controller: _passwordController, obscureText: true, validator: (value) => _passwordValidator(value), decoration: InputDecoration(hintText: 'Password'), ), SizedBox(height: 16.0), _isSigningIn ? CircularProgressIndicator() : ElevatedButton( onPressed: () async { if (_loginFormKey.currentState!.validate()) { setState(() { _isSigningIn = true; }); await _authentication.signInUsingEmailPassword( context: context, email: _emailController.text, password: _passwordController.text, ); setState(() { _isSigningIn = false; }); } }, child: const Text('Sign In'), ), SizedBox(height: 16.0), TextButton( onPressed: () => Navigator.of(context).push( MaterialPageRoute( builder: (context) => RegisterPage(), ), ), child: Text('Don\\'t have an account? Sign Up'), ), ], ), ), ), ); }

Inside the onPressed() callback of the Sign In button, the Firebase sign-in process is triggered (defined in the Authentication class).

A TextButton widget is added so that new users can navigate to the RegisterPage and create their accounts.

The RegisterPage code is quite similar to the LoginPage. Check the Stream Auth Firebase GitHub repo to find the user interface code for RegisterPage.

Implement Stream Messaging

You can easily integrate a full-fledged chat messaging service using Stream.

Stream channels pages for ChannelList and ChannelPage

First, you will need to add the ChannelListPage where users are navigated to after authenticated. This page will display a list of channels where the current user is a member.

Create a new file called channel_list_page.dart and add the following code:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ChannelListPage extends StatelessWidget { final Channel channel; ChannelListPage(this.channel); Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Stream Chat'), ), body: ChannelsBloc( child: ChannelListView( filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]), sort: [SortOption('last_message_at')], channelWidget: Builder( builder: (context) => ChannelPage(channel), ), ), ), ); } }

On this page, you can also add an action to the AppBar for signing out:

First, create an instance of the Authentication class:

dart
1
final _authentication = Authentication();

Add the Sign Out action like this:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
AppBar( title: Text('Stream Chat'), actions: [ PopupMenuButton<String>( onSelected: (_) async { await _authentication.signOut(); Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => LoginPage(), ), ); }, itemBuilder: (BuildContext context) { return [ PopupMenuItem<String>( value: 'sign_out', child: Text('Sign out'), ) ]; }, ), ], )

Next, you need to define a page to display messages and allow the user to send new messages.

Create a new file called channel_page.dart and add the following code:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ChannelPage extends StatelessWidget { final Channel channel; const ChannelPage(this.channel); Widget build(BuildContext context) { return Scaffold( appBar: ChannelHeader(), body: Column( children: <Widget>[ Expanded( child: MessageListView(), ), MessageInput(), ], ), ); } }

Congratulations 🎉 ! You successfully implemented serverless authentication to your Stream messaging app using Firebase.

Bonus: Auto Login

You will notice that the current version of the app requires the user to log in every time they launch the app after closing, even if they signed in previously.

To improve the user experience, you can automatically log in users if they were in a signed-in state in their previous session:

Add a new method to the Authentication class:

dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Future<bool> isSignedIn(BuildContext context) async { firebaseUser = auth.currentUser; bool isSignedIn = false; if (firebaseUser != null) { isSignedIn = true; try { final callable = functions.httpsCallable('getStreamUserToken'); final results = await callable(); String? token = results.data; if (token != null) { StreamClient.initialize(token, context); } } catch (e) { print('Error in fetching token: $e'); } } return isSignedIn; }

This method will return a boolean indicating whether the user was previously signed in.

Go to the LoginPage class and add the following method to check for signed-in users:

dart
1
2
3
4
5
6
7
8
9
10
11
bool _isCheckingUser = true; bool _isSignedIn = false; checkIfUserSignedIn() async { bool signedInState = await _authentication.isSignedIn(context); setState(() { _isCheckingUser = false; _isSignedIn = signedInState; }); }

Call this method in the initState of this class:

dart
1
2
3
4
5
void initState() { super.initState(); checkIfUserSignedIn(); }

As you start the app, it loads up the LoginPage class, where it checks if the user is already signed-in. If the user is signed-in previously, it automatically navigates to the ChannelListPage.

More to Explore

In this article, you learned how to implement Firebase authentication using email/password and integrate it with your Stream Messaging app.

But, you can use the same principles to integrate other authentication providers as well, like Google, Apple, Facebook, and GitHub.

Learn more about Stream and Firebase from these links:

You can find this sample app repository on the author's GitHub.

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->