Real-time chat without lag is a common headache for mobile teams. Misconfigured WebSocket, unstable reconnection, token leaks—these are typical issues we see during audits. The Stream Chat SDK solves them, but requires proper setup. With 5 years of experience and 20+ chat projects, 8 of which used Stream Chat, we guarantee stability even on weak connections. Let's walk through integration from scratch, skipping the "Hello World" from the docs.
Stream Chat differs from SendBird architecturally: its API is built on an event-driven model (WebSocket + event-driven state), and the SDK provides ready-made SwiftUI/Compose components with deep customization via subclassing and view factories. The binary size is smaller: iOS ~8 MB, Android AAR ~6 MB. We've often seen that SendBird offers more freedom, but Stream Chat is faster to integrate—up to 40% time savings on the initial setup.
| Criteria | Stream Chat | SendBird |
|---|---|---|
| SDK size (iOS) | ~8 MB | ~12 MB |
| API model | event-driven (WebSocket) | REST + WebSocket |
| Ready UI components | SwiftUI, Compose, UIKit | UIKit, SwiftUI (not all) |
| Offline cache | CoreData / Room (built-in) | Manual setup required |
| Price (basic tier) | $0.1/month per MAU | $0.2/month per MAU |
Why Stream Chat saves up to 40% development time?
Ready-made UI components cover 80% of the behavior. The remaining 20% is customization via ViewFactory. This is faster than writing your own chat from scratch with UIKit/Compose. Full custom UI only makes sense when design systems are incompatible.
Initialization and token management
Stream uses JWT. The client never generates a token itself—only your backend via Stream Chat SDK. On the backend, the token is created using stream_chat.create_token(user_id) (Python/Node SDK). On the client:
// iOS let client = ChatClient(config: ChatClientConfig(apiKeyString: "YOUR_KEY")) let token = try Token(rawValue: "eyJ...") client.connectUser(userInfo: .init(id: userId), token: token) { error in ... } // Android val client = ChatClient.Builder("YOUR_KEY", context).build() client.connectUser(User(id = userId), token).enqueue { result -> ... } Token refresh: Stream SDK calls TokenProvider when the token expires. Implement TokenProvider (iOS: closure-based, Android: TokenProvider interface)—there, make a request to your API and return a new token. Without this, the user will be disconnected after the token TTL.
How to create channels and subscribe to events?
Stream uses a type:id combination to identify channels. Types are messaging, livestream, team, commerce, gaming—they affect default permissions.
// iOS: get or create a 1-on-1 channel let channelId = ChannelId(type: .messaging, id: "user1_user2") let controller = client.channelController( createChannelWithId: channelId, members: [userId, targetId], isCurrentUserMember: true ) controller.synchronize { error in ... } synchronize() is the key call. It fetches history, subscribes to real-time events, and syncs the local state. Without it, the channel is created but events don't arrive. On Android, it's analogous via client.channel(channelType, channelId).create(memberIds).
How to set up push notifications?
Stream uses its own notification provider on top of APNs/FCM. Registration:
// iOS—after receiving APNs token chatClient.currentUserController().addDevice(.apns(token: deviceToken)) // Android FirebaseMessaging.getInstance().token.addOnSuccessListener { token -> client.addDevice(Device(token = token, pushProvider = PushProvider.FIREBASE)).enqueue() } Stream automatically sends notifications for new messages in channels the user is a member of. In the dashboard, configure notification templates. Deeplink—via handling CKNNotificationInfo (iOS) or RemoteMessage.data (Android).
UI customization: preserve functionality
Stream provides ChatChannelView, MessageListView, MessageComposerView from the StreamChatSwiftUI package. Customization is done via ViewFactory:
class CustomViewFactory: DefaultViewFactory { func makeMessageAvatarView(for userInfo: UserAvatarData) -> some View { // Your custom avatar CustomAvatarView(imageUrl: userInfo.imageURL) } } Utils.shared.viewFactory = CustomViewFactory() This is cleaner than a fully custom UI—80% of the behavior (swipes, reactions, threads) comes for free, you only customize the appearance. Full custom UI only makes sense if the design is incompatible with Stream's component model. For Android, it's similar: MessageListView and MessageComposerView in XML or Compose, customization via AttachmentFactoryManager and MessageListViewModelFactory.
Offline cache and reconnection
iOS SDK uses CoreData under the hood, Android uses Room. It's enabled automatically when isLocalStorageEnabled = true in the configuration (true by default). When the network is restored, the SDK automatically syncs missed events via the WebSocket health check mechanism.
What's included in the implementation?
We document every step and leave you with working code. Below are the stages and timelines.
| Stage | Duration |
|---|---|
| Stream app registration and key configuration | 1 day |
| Token endpoint implementation on the backend | 1 day |
| SDK integration (iOS/Android/Flutter) | 1–2 days |
| Choice between components and custom UI | 0.5 day |
| Push notifications and deep linking setup | 1 day |
| Testing reconnect/offline scenarios | 1 day |
| Documentation and code handover | 0.5 day |
Timelines and next steps
With ready-made StreamChatSwiftUI/StreamChatUI components—3–4 days. Fully custom UI on Core SDK—6–8 days. Pricing is individual. Contact us for an audit of your current implementation or order a turnkey integration—we guarantee stability and post-delivery support.
ViewFactory examples for iOS
You can customize not only the avatar but also message colors, fonts, and layout. Full list of ViewFactory methods is described in Stream's documentation.







