In this tutorial, we’ll build a fully functioning iOS app that uses Firebase for authentication and Stream for in-app chat, without writing a single line of server code. Thanks to the power of Firebase Extensions, we can automate all the backend plumbing that would typically require custom infrastructure.
With the Stream Firebase Extension, user creation and deletion events in Firebase are automatically synced with the Stream Chat backend via Cloud Functions. This means when a user signs up or is removed from your app, the corresponding Stream user is created or deleted without any manual intervention.
Without this extension, you’d need to manually sync Firebase Auth events to Stream via custom Cloud Functions and expose a secure token generation endpoint.
Even better, the extension includes a ready-to-use Cloud Function for generating Stream authentication tokens, so your frontend can securely connect to the chat service with minimal configuration.
We’ll walk through:
- Setting up Firebase Auth in an iOS app
- Configuring the Stream Firebase Extension and explaining what it really does
- Signing in a user with Firebase
- Watching as user data automatically syncs with Stream
- Loading and interacting with Stream Chat in the app
By the end, you’ll have a production-ready chat integration running entirely on Firebase + Stream—with no custom backend code.
You can either follow along and set it up from scratch, which is fairly straightforward, or clone our sample app from the repository.
Setting Up and Initializing Firebase
To get started, we need to set up our iOS app with Firebase and configure the necessary authentication tools.
Begin by creating a new Xcode project and naming it FirebaseExtensionDemo
. Then head over to the Firebase Console, and create a new Firebase project. Once the project is created, enable Authentication, go to the Sign-in method tab, and enable at least the Email/Password provider to allow users to register and log in.
Next, we’ll integrate Firebase into our iOS project using the Swift Package Manager. In Xcode, navigate to File > Add Package Dependencies, and paste the following repository URL: https://github.com/firebase/firebase-ios-sdk.git
Select the firebase-ios-sdk
package, and make sure to enable both FirebaseAuth
and FirebaseFunctions
in the "Add to Target" column for your app. Click Add Package to complete the integration.
Now it’s time to configure Firebase for your app. On your Firebase project dashboard, click on Add App and select iOS. You’ll be asked to enter the app’s Bundle ID, which you can find in your Xcode project settings. Once entered, download the GoogleService-Info.plist
file and drag it into the root of your Xcode project.
To initialize Firebase in your app, add the following code to your FirebaseExtensionDemoApp.swift
file, outside the @main
struct:
1234567class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { FirebaseApp.configure() return true } }
Finally, inside your main FirebaseExtensionDemoApp
SwiftUI struct, initialize the AppDelegate
like this:
1@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
With that, Firebase is now successfully integrated into your iOS app and ready to handle authentication and Cloud Functions.
Building a Signup and Login Experience
With Firebase configured, the next step is to allow users to sign up and sign in. We'll do this by building a simple authentication flow using SwiftUI and Firebase Auth.
We start by defining an AuthState
enum to capture the different states the authentication flow can be in—such as loading, signed in, signed out, or an error state with a message:
123enum AuthState { case loading, signedIn(userId: String), signedOut, error(message: String) }
Next, we create a basic FirebaseAuthViewModel
to encapsulate the login logic and represent our app’s authentication state. This follows the MVVM pattern commonly used in SwiftUI projects (great guide here). The ViewModel
adds a listener to Firebase Auth that observes changes to the user’s sign-in status:
12345678910111213141516171819202122232425262728293031323334353637@Observable class FirebaseAuthViewModel { private(set) var authState = AuthState.loading init() { Auth.auth().addStateDidChangeListener { auth, user in if let user { self.authState = .signedIn(userId: user.uid) } else { self.authState = .signedOut } } } func signIn(mail: String, password: String) { Auth.auth().signIn(withEmail: mail, password: password) { [weak self] authResult, error in if let error { self?.authState = .error(message: "Error occurred: \(error.localizedDescription)") return } guard let userId = authResult?.user.uid else { return } self?.authState = .signedIn(userId: userId) } } func signUp(mail: String, password: String) { Auth.auth().createUser(withEmail: mail, password: password) { [weak self] authResult, error in if let error { self?.authState = .error(message: "Error occurred: \(error.localizedDescription)") return } guard let userId = authResult?.user.uid else { return } self?.authState = .signedIn(userId: userId) } } }
Now let’s create a simple LoginView
using SwiftUI. It consists of a form for email and password input, as well as buttons to either sign in or sign up:
123456789101112131415161718192021222324252627282930313233343536struct LoginView: View { @State var viewModel: FirebaseAuthViewModel @State private var mail = "" @State private var password = "" var body: some View { VStack { Text("Login / Signup") .font(.title) Form { TextField("Mail", text: $mail) TextField("Password", text: $password) } HStack { Button { viewModel.signUp(mail: mail, password: password) } label: { Text("Sign Up") } .buttonStyle(.borderedProminent) Spacer() Button { viewModel.signIn(mail: mail, password: password) } label: { Text("Sign In") } .buttonStyle(.borderedProminent) } } } }
The final step is to react to the authentication state changing in our app. For that, we open ContentView
and replace the content with the following code:
12345678910111213141516171819202122232425struct ContentView: View { @State var viewModel = FirebaseAuthViewModel() var body: some View { switch viewModel.authState { case .loading: ZStack { ProgressView() } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) case .error(let message): ZStack { Text(message) .foregroundStyle(.red) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) case .signedIn(let userId): Text("Signed in") case .signedOut: LoginView(viewModel: viewModel) .padding() } } }
This takes the viewModel
’s authState
property and shows:
.loading
case: aProgressView
indicating a loading state.error
case: the error message in red.signedIn
case: aText
view for now, we will replace this later.signedOut
case: theLoginView
we just created
With these components in place, your app now supports Firebase-powered sign-up and login with just a few lines of Swift code. Firebase handles all the heavy lifting around user management, while your UI stays clean and reactive.
Installing the Stream Firebase Extension
With Firebase Auth set up and a sign-in experience in place, it’s time to connect our app to Stream Chat.
To do this without writing any backend logic, we’ll install the Stream Firebase Extension, which automates user creation, deletion, and token generation on the Stream backend—all triggered by Firebase Auth events.
Before installing the extension, make sure you have a Stream project ready. Head over to the Stream Dashboard, create a new project (or use an existing one), and take note of your API Key and API Secret, which can be found in the Overview section.
Now, go to your Firebase Console and locate the Extensions tab in the left-hand menu. Click Explore Extensions Hub, search for Authenticate with Stream Chat, and select Install in Firebase Console. This will bring you back to your Firebase dashboard to complete the setup.
You’ll be guided through several steps during the installation process:
- Review billing and usage – Note that a Blaze plan is required to use Firebase Extensions.
- Review APIs and resources – Firebase will enable the required APIs and create the necessary backend infrastructure.
- Review access permissions – The extension will need permission to access Authentication, Secret Manager, and Cloud Functions.
- Configure your Stream credentials – Enter your Stream API Key and Secret in the designated fields, and make sure to hit Add Secret to securely store them in Firebase’s Secret Manager.
- Skip advanced parameters – You can leave the advanced configuration section untouched unless you have custom needs.
Click Install Extension, and Firebase will take a few minutes to provision everything.
Once completed, your Firebase project will be equipped with a set of Cloud Functions that:
- Automatically create a user on the Stream backend whenever a Firebase user is created
- Automatically delete the Stream user when the Firebase user is deleted
- Provide a callable Cloud Function that returns a Stream auth token for the current Firebase user
At this point, Firebase and Stream are now linked—no server code required. You're ready to move on and start syncing real-time chat data with Stream.
Using the Stream Firebase Extension in Your App
Now that the Stream Firebase Extension is installed and working in the background, it’s time to put it to use.
After a user signs up or logs in through Firebase Auth, we want to seamlessly transition them into a real-time chat experience powered by Stream.
But before we load the chat UI, we first need to authenticate the user with Stream. This involves requesting an authentication token—normally something you'd have to build a secure server for. Fortunately, the Firebase Extension has already set up a Cloud Function that handles this securely for us.
Let’s walk through how to connect all the pieces.
First, install the Stream Chat iOS SDK by following the steps outlined in the official integration guide. Once installed, we need to initialize both the StreamChat
and the ChatClient
. We’ll do both of these steps inside our FirebaseExtensionsDemoApp.swift
file. We open it up and add the following code inside the FirebaseExtensionsDemoApp
struct (don’t forget to import StreamChat
and StreamChatSwiftUI
at the top):
1234567891011121314@State var streamChat: StreamChat? var chatClient: ChatClient = { var config = ChatClientConfig(apiKey: .init("<your-api-key>")) config.isLocalStorageEnabled = true config.applicationGroupIdentifier = "<your-bundle-id>" let client = ChatClient(config: config) return client }() init() { streamChat = StreamChat(chatClient: chatClient) }
Once initialized, create a new Swift file called StreamAuthViewModel
, which will handle the user connection logic.
Inside the view model, we define one property (a reference to the Firebase functions
property that we want to interact with when connecting a user) and two functions:
12345678910111213141516171819202122232425262728293031323334353637@Observable class StreamAuthViewModel { var functions = Functions.functions() func connectUser(with id: String, to client: ChatClient) async { functions.httpsCallable("ext-auth-chat-getStreamUserToken").call(["uid": id]) { result, error in if let error { log.error("Error: \(error)") return } guard let resultString = result?.data as? String, let token = try? Token(rawValue: resultString) else { log.error("The result returned from the Cloud Function was malformed.") return } // Call `connectUser` on our SDK to get started. client.connectUser( userInfo: .init( id: id, ), token: token ) { error in if let error = error { log.error("connecting the user failed \(error)") return } } } } func disconnectUser(from client: ChatClient) async { await client.disconnect() } }
The connectUser
method calls the ext-auth-chat-getStreamUserToken
Cloud Function, automatically provided by the Firebase Extension, and retrieves a valid Stream auth token. Once the token is returned, we use it to connect the user to the Stream Chat client.
Next, create a ChatView
in SwiftUI that takes a userId
and initializes the ChatViewModel
. We use the .task
modifier to call connectUser
asynchronously when the view appears, and the disconnectUser
when the view disappears:
123456789101112131415161718struct ChatView: View { var userId: String @State private var viewModel = ChatViewModel() var body: some View { ChatChannelListView() .task { await viewModel.connectUser(with: userId) } .onDisappear { Task { await viewModel.disconnectUser(from: chatClient) } } } }
Finally, update your app’s ContentView
to load the ChatView
when a user is successfully signed in. You can do this by modifying the switch
case for .signedIn
like so:
12case .signedIn(let userId): ChatView(userId: userId)
With that, you now have a fully functional chat experience integrated with Firebase Auth and powered by Stream, all without writing any custom backend logic.
Wrapping Up
In this tutorial, we built an iOS app that seamlessly combines Firebase Authentication and Stream Chat—all without writing a single line of backend code. By leveraging Stream's Firebase Extension, we offloaded the typical server-side responsibilities to a set of powerful, pre-built Cloud Functions.
Whenever a new user signs up through Firebase Auth, the extension automatically mirrors that user in the Stream Dashboard using the same user ID and metadata. The extension also provides a callable Cloud Function that returns a valid Stream auth token, allowing us to authenticate users securely from the client side.
This setup lets us focus entirely on building a rich and responsive app experience, while Firebase and Stream handle the heavy lifting behind the scenes. Whether you're building a prototype or a production-grade chat feature, this serverless approach drastically reduces complexity and gets you up and running in no time.