This tutorial teaches you how to build a fully featured Flutter messaging app with the Stream Chat SDK - channel list, message composer, reactions, threads, typing indicators, and offline support. You can use it as the foundation to build any type of in-app chat or messaging.
On the right is a preview of the finished app. A complete sample app is also available in our repo.
This page works for both humans and AI coding agents: every step is a file operation or shell command, every step has a verification checkpoint, and the few steps that need a human are explicitly marked.
Your app runs on Stream's edge network for optimal performance, across multiple pricing tiers including a free maker plan.
Choose your path
This tutorial has two paths. In Path A you let your AI agent build the app for you. In Path B you build it yourself, step by step. Both paths end with a working chat app - Path A's is shaped by what you ask for, Path B's by the steps below - so pick one instead of working through both.
Path A - Let your AI agent build it (recommended)
Install the Stream CLI once, then add the skills. This gives Claude Code, Cursor, or Codex the Flutter integration patterns and current SDK APIs, so it builds against real docs instead of stale training data.
123456789# Install the getstream CLI curl -fsSL https://getstream.io/cli.sh | bash # Install the skills (router, docs, and builder) for your agent. Pick the # target: --universal for Cursor, Codex, and other AGENTS-ecosystem tools, # or --claude for Claude Code. The Flutter pack installs on demand the first # time it's needed, or add it explicitly: getstream skills --universal getstream skills stream-flutter --universal
Then ask your agent:
1234/stream-flutter Build a Flutter chat app with a channel list, a channel screen and a thread screen. Provision credentials with the CLI: create or select my org and app, then mint a token. If you can't mint one, fall back to the demo credentials in https://getstream.io/chat/sdk/flutter/tutorial.md
Only the first line matters - /stream-flutter Build a Flutter chat app is enough to get going. The rest just steers the result: ask for a different screen split, your own theme, custom message bubbles, or a search screen before the channel list. Describe the UI you want rather than the UI the tutorial builds.
The agent pauses and hands back to you twice, both during setup: browser sign-in during CLI setup, and picking which org and app to use during getstream init. After that it scaffolds and builds the app itself - though depending on what your agent is allowed to run, it may ask you to run flutter create or flutter run yourself.
Already have your Flutter project? You can get the sign-in out of the way first: run getstream init from inside the project before you prompt the agent, and it picks up the initialized project from there.
Checkpoint: build and run the app if the agent hasn't (flutter run). You should see a channel list, be able to open a channel and send a message, and be able to tap a reply to open the thread screen.
Skim Important Building Blocks if you want to understand what the agent wrote before you extend it.
Path B - Build it manually
Follow the steps below. Every code block is a complete file or an explicitly scoped edit - each one says which.
For AI assistants reading this page: append
.mdto any Stream docs URL for a clean Markdown version. A condensed index for the Flutter SDK is athttps://getstream.io/chat/docs/sdk/flutter/llms.txt. Prefer these over parsing HTML.
Important Building Blocks
Three pieces, one mental model:
StreamChatClient- Low-level API client. Handles auth, websockets, and (once you attachstream_chat_persistence) offline storage. Created once, at app launch.StreamChat- The widget that wraps your app. Owns the theme and the component overrides, and hands them plus the client to every Stream widget below it throughStreamChat.of(context). Every Stream widget needs it as an ancestor.- UI components -
StreamChannelListViewfor the list,StreamMessageListView+StreamMessageComposerfor a conversation. Use them as-is, theme them, or swap individual slots viaStreamComponentBuilders.
You'll get a working app with the default UI first, then theme it, then customize the avatar. Deeper customizations are linked at the end.
Prerequisites
- Flutter 3.41 (or later) on the stable channel, with Dart 3.11 or later - required by the Stream Chat Flutter SDK. No Flutter yet? Follow the official install guide and confirm with
flutter doctor; on an existing install,flutter channel stable && flutter upgrade - Android
minSdk24 and iOS deployment target 13.0 - required by the SDK's media plugins. A project scaffolded by current Flutter already meets both, so this only matters when adding Stream to an older app - Stream Chat Flutter SDK version 10.2.0 (or latest) - you add this in Step 2
Step 1 - Create the project
Human checkpoint: you create the project, not your agent. Run:
12flutter create --empty --org io.getstream.tutorial --project-name chat_tutorial --platforms android,ios chat_tutorial cd chat_tutorial
--empty scaffolds a minimal lib/main.dart, which Step 4 replaces wholesale anyway. You can use your IDE's new-Flutter-project flow instead - if you do, delete anything it generates under test/, since those tests exercise starter code Step 4 removes.
Agents: detect the project shape first (
find . -maxdepth 2 -name "pubspec.yaml"). If a Flutter project already exists, work in place instead of scaffolding a second one. If there is no project, stop and ask the human to create it - don't scaffold it yourself.
Checkpoint:
12flutter --version # 3.41 or later flutter analyze # No issues found!
Step 2 - Add the SDK dependency
Add the UI package and the offline-storage package:
1flutter pub add stream_chat_flutter stream_chat_persistence
Your pubspec.yaml now has caret constraints on the current 10.x release:
123456dependencies: flutter: sdk: flutter stream_chat_flutter: ^10.2.0 stream_chat_persistence: ^10.2.0
Then pin the Android Gradle Plugin to the 8.x line in android/settings.gradle.kts:
12345plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.13.0" apply false id("org.jetbrains.kotlin.android") version "2.3.20" apply false }
Why the pin: Flutter's current template ships AGP 9, which the Android plugin ecosystem is still catching up to - on an unpinned project the build stops at
cannot find symbol: class FilePickerPlugin. AGP 8.13 keeps every plugin in the tree on the same toolchain. Remove the pin once your plugins all build against AGP 9.
No iOS build changes are needed - a project scaffolded by current Flutter already targets iOS 13.0 and pulls Stream's native dependencies through Swift Package Manager, so there's no Podfile to edit.
Checkpoint:
123flutter pub get # resolves stream_chat_flutter 10.2.x flutter build apk --debug # ✓ Built .../app-debug.apk flutter build ios --no-codesign # ✓ Built .../Runner.app (macOS only)
Step 3 - Get your credentials
You need an API key and a user token, both belonging to your own Stream app. The getstream CLI provisions them in one flow. Run these from the chat_tutorial directory you created in Step 1 - the CLI stores project credentials there.
1. Install the CLI (skip if you did this in Path A):
1curl -fsSL https://getstream.io/cli.sh | bash
2. Initialize the project. This one command authenticates you, lets you create or select an organization and app, and writes the project credentials. New to Stream? The sign-in flow creates your organization. Already have an org or an app? It lets you pick them.
1getstream init
Human checkpoint: getstream init opens a browser to authenticate. Agents: run it, then ask the human to finish logging in before continuing. It's required first - env, token, and api commands fail with "stream project is not initialized" until it runs.
3. Write your API key into the Flutter project. This writes the public API key to dart_defines.json (which it adds to .gitignore for you) instead of hardcoding it in source. The API secret is never printed or written into the app. You read it via String.fromEnvironment('STREAM_API_KEY') in Step 4 and run the app with flutter run --dart-define-from-file=dart_defines.json.
1getstream env --target flutter
4. Mint a user token for a user in your app (never-expiring by default; add a TTL for production-like testing):
12getstream token tutorial_user getstream token tutorial_user --ttl 1d
5. (Optional) Seed a channel so your first launch isn't an empty list. Create the users first, then the channel:
1234getstream api UpdateUsers --request '{"users":{"tutorial_user":{"id":"tutorial_user","name":"Tutorial User"},"alice":{"id":"alice","name":"Alice"}}}' getstream api GetOrCreateChannel --type messaging --id general \ --request '{"data":{"created_by_id":"tutorial_user","members":[{"user_id":"tutorial_user"},{"user_id":"alice"}]}}'
Checkpoint: dart_defines.json contains a STREAM_API_KEY entry, and you have a token printed by the CLI, both belonging to your own app. Keep the token for Step 4.
Fallback: tutorial demo credentials
Want to skip account setup entirely? These work against Stream's shared, pre-seeded tutorial environment:
- API key -
b67pax5b2wdq - User ID -
tutorial-flutter - Token -
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZmx1dHRlciJ9.S-MJpoSwDiqyXpUURgO5wVqJ4vKlIVFLSEyrFYCOE1c
Swap in your own credentials from the CLI flow above before building anything real.
On these, launch with plain flutter run - not the --dart-define-from-file command shown later. That file only exists after getstream env, and passing the flag without it is a hard error.
Step 4 - Get a working app
Four files under lib/, each complete: the client setup, plus three screens - channel list, conversation, and thread - composed from Stream's widgets. Wiring them together yourself is what makes any one of them easy to swap later.
Start with lib/main.dart. It builds the client, turns on offline storage, connects the user, and puts StreamChat above the app.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; import 'channel_list_page.dart'; /// Credentials from Step 3. /// - API key: `getstream env --target flutter` writes it to `dart_defines.json`, /// passed in with `--dart-define-from-file` and read here. Falls back to the demo key. /// - User + token: set `userId` to the user you minted a token for and paste that /// token below - both must match, or the connection is rejected. Or keep the /// demo pair below as-is. const _envApiKey = String.fromEnvironment('STREAM_API_KEY'); const apiKey = _envApiKey == '' ? 'b67pax5b2wdq' : _envApiKey; const userId = 'tutorial-flutter'; const userName = 'Tutorial Flutter'; const userToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZmx1dHRlciJ9.S-MJpoSwDiqyXpUURgO5wVqJ4vKlIVFLSEyrFYCOE1c'; Future<void> main() async { /// Offline support: channels and messages are cached on device, so the app /// opens with content even without a connection. Attach the persistence /// client *before* `connectUser` - attaching it afterwards does nothing for /// the current session. final client = StreamChatClient(apiKey, logLevel: Level.INFO) ..chatPersistenceClient = StreamChatPersistenceClient( logLevel: Level.INFO, connectionMode: ConnectionMode.regular, ); /// Development token from `getstream token`. In production, fetch the /// token from your backend after login - never hardcode secrets. await client.connectUser( User(id: userId, name: userName), userToken, ); runApp(MyApp(client: client)); } class MyApp extends StatelessWidget { const MyApp({super.key, required this.client}); /// The client created in `main`. Holds the connection and the local cache. final StreamChatClient client; Widget build(BuildContext context) { return MaterialApp( /// `StreamChat` must be an ancestor of every Stream widget. Putting it in /// `builder` keeps it above whatever `home` renders. builder: (context, child) => StreamChat(client: client, child: child), home: const ChannelListPage(), ); } }
Then lib/channel_list_page.dart, the entry screen. StreamChannelListController owns the query, pagination, and live updates; StreamChannelListView renders it.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'channel_page.dart'; /// Displays the channels the current user is a member of. class ChannelListPage extends StatefulWidget { const ChannelListPage({super.key}); State<ChannelListPage> createState() => _ChannelListPageState(); } class _ChannelListPageState extends State<ChannelListPage> { /// Queries channels the current user belongs to, newest activity first. /// The controller owns pagination and live updates. late final _listController = StreamChannelListController( client: StreamChat.of(context).client, filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]), channelStateSort: const [SortOption.desc('last_message_at')], limit: 20, ); void dispose() { _listController.dispose(); super.dispose(); } Widget build(BuildContext context) { return Scaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: const StreamChannelListHeader(), body: StreamChannelListView( controller: _listController, onChannelTap: (channel) => Navigator.of(context).push( MaterialPageRoute( /// `StreamChannel` scopes the tapped channel to the subtree and /// calls `watch()` on it, so `ChannelPage` needs no arguments. builder: (_) => StreamChannel( channel: channel, child: const ChannelPage(), ), ), ), ), ); } }
Next lib/channel_page.dart, a single conversation. This is where reactions, attachments, typing indicators, URL previews, and read state all come for free.
123456789101112131415161718192021222324252627282930import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'thread_page.dart'; /// Displays the messages inside a single channel. class ChannelPage extends StatelessWidget { const ChannelPage({super.key}); Widget build(BuildContext context) { return Scaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: const StreamChannelHeader(), body: Column( children: <Widget>[ Expanded( /// Threads are opt-in: with no `threadBuilder` (and no /// `onThreadTap`), tapping a reply does nothing at all. child: StreamMessageListView( threadBuilder: (_, parentMessage) => ThreadPage(parent: parentMessage!), ), ), StreamMessageComposer(), ], ), ); } }
Finally lib/thread_page.dart. The threadBuilder above points here, and the SDK handles the navigation.
123456789101112131415161718192021222324252627282930313233343536373839404142434445import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Displays the replies to a single parent message. class ThreadPage extends StatefulWidget { const ThreadPage({super.key, required this.parent}); /// The message this thread hangs off. final Message parent; State<ThreadPage> createState() => _ThreadPageState(); } class _ThreadPageState extends State<ThreadPage> { /// Seeding the composer with `parentId` is what makes a sent message a /// thread reply instead of a new channel message. late final _composerController = StreamMessageComposerController( message: Message(parentId: widget.parent.id), ); void dispose() { _composerController.dispose(); super.dispose(); } Widget build(BuildContext context) { return Scaffold( backgroundColor: context.streamColorScheme.backgroundApp, appBar: StreamThreadHeader(parent: widget.parent), body: Column( children: <Widget>[ Expanded( child: StreamMessageListView(parentMessage: widget.parent), ), StreamMessageComposer( messageComposerController: _composerController, ), ], ), ); } }
Run it with the API key from Step 3:
1flutter run --dart-define-from-file=dart_defines.json
Checkpoint: the channel list loads with no errors in the console. Tap a channel -> the conversation opens. Send a message, long-press it for reactions, tap a reply to open a thread, and paste a link to see it unfurl. Type @ for the mention autocomplete and / for slash commands - both are wired into the composer already. Empty list but no error? Your user has no channels yet - seed one with the getstream api GetOrCreateChannel command from Step 3.

Optional - enable photo, camera, and voice attachments. The composer browses the device gallery in-app, which needs media access declared on both platforms.
Add the usage descriptions to ios/Runner/Info.plist:
123456<key>NSCameraUsageDescription</key> <string>Used to capture photos for chat messages.</string> <key>NSPhotoLibraryUsageDescription</key> <string>Used to attach images to chat messages.</string> <key>NSMicrophoneUsageDescription</key> <string>Used to record voice messages.</string>
And the media permissions to android/app/src/main/AndroidManifest.xml, above <application>:
123<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/> <uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
Voice recording needs nothing extra on Android - the SDK's recorder plugin merges RECORD_AUDIO in for you.
Prefer not to declare media permissions at all? Hand the composer the system picker instead. It needs no Android permissions, at the cost of the in-app gallery grid:
1StreamMessageComposer(useSystemAttachmentPicker: true)
Step 5 - Theme the app
Theming works in two layers, and you rarely need more than the first:
- Design tokens - a
StreamThemeregistered as aThemeDataextension. Give it a brand color and Stream derives its whole semantic palette from that swatch. - Per-widget overrides - a
StreamChatThemeDatapassed toStreamChat.themeData, merged on top. Reach for this only when one component needs to differ.
Both go in MyApp. Replace the class in lib/main.dart:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455class MyApp extends StatelessWidget { const MyApp({super.key, required this.client}); /// The client created in `main`. Holds the connection and the local cache. final StreamChatClient client; Widget build(BuildContext context) { /// One brand color per brightness. Stream derives its whole semantic /// palette from the swatch, so this single value restyles bubbles, /// sending indicators, unread badges, and the composer cursor. final brand = StreamColorSwatch.fromColor(Colors.green); final brandDark = StreamColorSwatch.fromColor( Colors.green, brightness: Brightness.dark, ); /// Per-widget override, merged on top of the derived palette. Reusing /// `brand.shade100` is what keeps the tiles and the message bubbles in /// the same green family. final customTheme = StreamChatThemeData( channelListItemTheme: StreamChannelListItemThemeData( titleStyle: const TextStyle(fontWeight: FontWeight.bold), backgroundColor: WidgetStateProperty.all(brand.shade100), ), ); return MaterialApp( theme: ThemeData( brightness: Brightness.light, extensions: [ StreamTheme( brightness: Brightness.light, colorScheme: StreamColorScheme.light(brand: brand), ), ], ), darkTheme: ThemeData( brightness: Brightness.dark, extensions: [ StreamTheme( brightness: Brightness.dark, colorScheme: StreamColorScheme.dark(brand: brandDark), ), ], ), builder: (context, child) => StreamChat( client: client, themeData: customTheme, child: child, ), home: const ChannelListPage(), ); } }
Checkpoint: the whole app turns green - outgoing bubbles, the sending indicator, unread badges, and the composer cursor all pick up the brand swatch, and channel tiles get the lighter tint with bold titles. Switch your device to dark mode and the dark swatch takes over.
Full theming reference: StreamChat and theming.

Step 6 - Customize one piece of the message list
Theming changes tokens (colors, fonts, shapes). To change an actual widget, register a component builder and override only the slot you want - every other widget keeps its default. Here we swap the circular avatar for a rounded square.
Avatar shape isn't a theme token, so this is a slot job rather than a Step 5 job.
Create lib/rounded_avatar.dart:
12345678910111213141516171819202122232425262728293031323334import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// A rounded-square avatar to replace the SDK's circular one. class RoundedAvatar extends StatelessWidget { const RoundedAvatar({super.key, required this.props}); /// Everything the SDK would have used to draw the default avatar. final StreamAvatarProps props; Widget build(BuildContext context) { final imageUrl = props.imageUrl; final size = props.size?.value ?? StreamAvatarSize.lg.value; return ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), child: SizedBox.square( dimension: size, child: ColoredBox( color: props.backgroundColor ?? context.streamColorScheme.backgroundApp, child: imageUrl == null ? Center(child: props.placeholder(context)) : Image.network( imageUrl, fit: BoxFit.cover, errorBuilder: (context, _, _) => Center(child: props.placeholder(context)), ), ), ), ); } }
Reusing props.placeholder keeps the SDK's initials fallback for users with no picture, and props.size keeps every call site's sizing intact.
Then register it on StreamChat in lib/main.dart - add the import and the componentBuilders argument:
123456789101112import 'rounded_avatar.dart'; // ... builder: (context, child) => StreamChat( client: client, themeData: customTheme, componentBuilders: StreamComponentBuilders( avatar: (context, props) => RoundedAvatar(props: props), ), child: child, ),
That's the whole pattern: build a widget that takes the slot's props, then hand it to StreamComponentBuilders. The same approach replaces the message item, the composer, attachments, and more.
avataris one of the shared slots you pass directly. Chat-specific ones -messageItem,channelListItem,messageComposer- go throughextensions: streamChatComponentBuilders(...)instead. The analyzer tells you when a slot belongs to the other list.
Checkpoint: avatars are rounded squares instead of circles. Because avatar is a single global slot, the change lands in the message rows, the channel list, and the headers at once - without touching any of those widgets.
![]()
Customize further - cookbooks
For larger changes, Stream has full cookbooks. Each rebuilds a screen from smaller pieces instead of styling the pre-built one.
Start here
- Cookbook overview - the index of recipes
- Customizing widgets - the pattern for swapping any slot, including channel list items and empty states
Channel list
- Custom channel list - build a bespoke list on the headless controllers
- Creating channels - make channels from app code instead of the CLI
Chat / message view
- Custom message list - the same for the conversation
- Avatars, reactions, and custom message actions
Verify the whole build
12flutter analyze flutter run --dart-define-from-file=dart_defines.json
Human checkpoint: launching on a simulator or device and confirming the UI is a human step. Agents: run flutter analyze and build, then hand back to the human to run it. Confirm the full loop: channel list loads -> open a channel -> send a message -> long-press for reactions -> open a thread. Then enable airplane mode and reopen the app: the channel list and recent messages still render from the local cache.
Troubleshooting
stream project is not initialized- CLI onboarding not run. Rungetstream initin the project directory first.getstream envreturns 401 - the CLI session expired. Rungetstream login, then retry.cannot find symbol: class FilePickerPlugin- an Android plugin in the dependency tree doesn't build against AGP 9 yet. Pin AGP to8.13.0inandroid/settings.gradle.kts(Step 2).pub getresolves an older SDK than 10.2.0 - your Flutter is below 3.41, sopubfalls back to an older release whose APIs don't match this tutorial. Runflutter upgradeon the stable channel.- Blank screen after launch -
connectUserfailed. Check the console; confirm the API key and token belong to the same app. token is invalid/ auth error - the key and the token belong to different apps, or the token expired. If you provisioned your own app, first check you launched with--dart-define-from-file=dart_defines.json- without it the app falls back to the demo key while still using your token, which fails exactly this way. Otherwise re-mint withgetstream token <user_id>and confirm the API key matches.- Channel list is empty, no errors - the user has no channels. Seed one with
getstream api GetOrCreateChannel(Step 3), and make sure the token user is indata.members. Did not find the file passed to "--dart-define-from-file"-dart_defines.jsondoesn't exist becausegetstream envnever ran. On the demo credentials, launch with plainflutter run- the code falls back to the demo key when the define is absent.- Tapping a reply does nothing -
StreamMessageListViewhas nothreadBuilder(oronThreadTap). Threads are opt-in; wire it as in Step 4. StreamChat.of() called with a context that does not contain a StreamChat- a Stream widget is outside theStreamChatancestor, or you used the samecontextthat created it. KeepStreamChatinMaterialApp.builder.- Offline cache stays empty -
chatPersistenceClientwas assigned afterconnectUser. Attach it with the cascade before connecting (Step 4). - Attachment or voice button crashes on iOS - missing
Info.plistusage descriptions. Add the keys from the Step 4 note. - Gallery tab offers "Allow access to your gallery" but system settings has no permission to grant - the
READ_MEDIA_*permissions aren't inAndroidManifest.xml. Add them (Step 4 note) and the runtime prompt appears normally.
Next steps
- AI chat experiences - add an AI assistant to a channel with streaming responses and typing indicators: AI integration
- Video & audio calls - the Video Flutter SDK integrates with chat: chat integration guide
- Push notifications - Flutter push setup
- Sample app - the complete demo: Flutter Sample App on GitHub
Pricing is flexible with multiple tiers, including a free maker plan for hobby projects and small companies.
Video / audio room integration
For a complete social experience, Stream provides a Video & Audio calling Flutter SDK, that works seamlessly with our chat products. If you want to learn more on how to integrate video into your apps, please check our docs and our tutorials about video calling and livestreaming.
We also have a guide on how to integrate video with chat.
Final Thoughts
We have shown you how to build a fully featured in-app chat experience with Flutter, which includes reactions, threads, typing indicators, offline storage, URL previews, user presence and more. It's pretty crazy how APIs & Flutter components enable you to build chat in hours. On top of that you now know how easy it is to add your own theme to the app and even fully customize its key components.
The chat app we built uses Stream's edge network for optimal performance and scalability. Stream powers thousands of apps and over a billion end users. There are several price tiers available, including a free plan for development and a free maker plan.
Both the Chat SDK for Flutter and the API have plenty more features available to support more advanced use-cases such as push notifications, content moderation, rich messages and more. And for fully custom experiences, stream_chat_flutter_core exposes the same state and controllers without any UI of its own.
Machine-readable resources
For AI agents and coding assistants working with this SDK:
- CLI + skills:
curl -fsSL https://getstream.io/cli.sh | bash, thengetstream skills --universalandgetstream skills stream-flutter --universalfor the Flutter pack (use--claudeinstead for Claude Code). (Alternative:npx skills add GetStream/agent-skills -s stream.) - Provisioning:
getstream init(auth + create/select org & app) ->getstream env --target flutter(API key todart_defines.json) ->getstream token <user_id>(mint a token) - Data & config from the CLI:
getstream api <Endpoint> --request '{...}'for users, channels, and messages - Flutter integration skill: invoke
/stream-flutterin your agent for Flutter setup patterns;/stream-docssearches live SDK docs with citations - Docs index for LLMs:
https://getstream.io/chat/docs/sdk/flutter/llms.txt(condensed) - Markdown endpoints: append
.mdto any docs URL for a clean, token-efficient version - Source of truth for APIs: the SDK repository - check the pinned version's source rather than assuming APIs from training data

