To connect Supabase to Flutter, add the supabase_flutter package to your pubspec.yaml, initialize it in main() with your project URL and anon key, then access the client via Supabase.instance.client throughout your app. The Flutter SDK handles session persistence with shared preferences automatically. You can perform auth, database queries, storage operations, and realtime subscriptions using the same API patterns as the JavaScript client.
Connecting a Flutter App to Supabase
This tutorial shows you how to integrate Supabase into a Flutter application using the official supabase_flutter package. You will set up the client, implement email/password authentication, perform database CRUD operations, and handle auth state changes. The Flutter SDK provides a native Dart API that mirrors the JavaScript client, so concepts transfer directly between platforms.
Prerequisites
- Flutter SDK installed (3.10+)
- A Supabase project with your URL and anon key
- A code editor with Flutter/Dart support
- Basic Dart programming knowledge
Step-by-step guide
Add the supabase_flutter dependency
Add the supabase_flutter dependency
Add supabase_flutter to your Flutter project's pubspec.yaml file. This package includes the full Supabase client for Dart plus Flutter-specific features like deep link handling and automatic session persistence using shared preferences. Run flutter pub get after adding the dependency.
1# pubspec.yaml2dependencies:3 flutter:4 sdk: flutter5 supabase_flutter: ^2.0.067# Then run:8flutter pub getExpected result: The supabase_flutter package is installed and available for import in your Dart code.
Initialize Supabase in main()
Initialize Supabase in main()
Initialize the Supabase client before running your app by calling Supabase.initialize() in your main() function. This must be called before runApp() and requires WidgetsFlutterBinding.ensureInitialized() since it accesses platform channels. Pass your Supabase project URL and anon key. The anon key is safe for client-side use because it respects Row Level Security policies.
1import 'package:flutter/material.dart';2import 'package:supabase_flutter/supabase_flutter.dart';34void main() async {5 WidgetsFlutterBinding.ensureInitialized();67 await Supabase.initialize(8 url: 'https://your-project-ref.supabase.co',9 anonKey: 'your-anon-key-here',10 );1112 runApp(const MyApp());13}Expected result: Supabase is initialized and the client is accessible via Supabase.instance.client anywhere in the app.
Implement email/password authentication
Implement email/password authentication
Use the Supabase client to sign up new users and sign in existing ones. The API mirrors the JavaScript client: signUp for registration, signInWithPassword for login, and signOut for logout. The Flutter SDK automatically persists the session using shared preferences, so users stay logged in between app restarts.
1final supabase = Supabase.instance.client;23// Sign up a new user4Future<void> signUp(String email, String password) async {5 final response = await supabase.auth.signUp(6 email: email,7 password: password,8 );9 if (response.user == null) {10 throw Exception('Sign up failed');11 }12}1314// Sign in with email and password15Future<void> signIn(String email, String password) async {16 final response = await supabase.auth.signInWithPassword(17 email: email,18 password: password,19 );20 if (response.session == null) {21 throw Exception('Sign in failed');22 }23}2425// Sign out26Future<void> signOut() async {27 await supabase.auth.signOut();28}Expected result: Users can sign up, sign in, and sign out. Sessions are automatically persisted across app restarts.
Listen to auth state changes
Listen to auth state changes
Subscribe to auth state changes to reactively update your UI when users sign in or out. The onAuthStateChange stream emits AuthState objects containing the event type and current session. Use this in a StreamBuilder widget or a state management solution to navigate between auth and main screens automatically.
1class AuthGate extends StatelessWidget {2 const AuthGate({super.key});34 @override5 Widget build(BuildContext context) {6 return StreamBuilder<AuthState>(7 stream: Supabase.instance.client.auth.onAuthStateChange,8 builder: (context, snapshot) {9 if (snapshot.connectionState == ConnectionState.waiting) {10 return const Center(child: CircularProgressIndicator());11 }1213 final session = snapshot.data?.session;14 if (session != null) {15 return const HomePage();16 } else {17 return const LoginPage();18 }19 },20 );21 }22}Expected result: The app automatically navigates between login and home screens based on auth state.
Perform database CRUD operations
Perform database CRUD operations
Query and modify data using the Supabase client. The Dart API uses the same method chain pattern as JavaScript: from(), select(), insert(), update(), delete() with filters like eq(). Remember that all operations go through RLS — you must have appropriate policies on your tables for the operations to succeed.
1final supabase = Supabase.instance.client;23// Fetch all todos for the current user4Future<List<Map<String, dynamic>>> getTodos() async {5 final response = await supabase6 .from('todos')7 .select()8 .order('created_at', ascending: false);9 return List<Map<String, dynamic>>.from(response);10}1112// Insert a new todo13Future<void> addTodo(String title) async {14 await supabase.from('todos').insert({15 'title': title,16 'is_complete': false,17 'user_id': supabase.auth.currentUser!.id,18 });19}2021// Update a todo22Future<void> toggleTodo(int id, bool isComplete) async {23 await supabase24 .from('todos')25 .update({'is_complete': isComplete})26 .eq('id', id);27}2829// Delete a todo30Future<void> deleteTodo(int id) async {31 await supabase.from('todos').delete().eq('id', id);32}Expected result: Your Flutter app can create, read, update, and delete data from Supabase tables with RLS enforcement.
Set up RLS policies for your Flutter app
Set up RLS policies for your Flutter app
Enable Row Level Security on your tables and write policies so that authenticated Flutter users can only access their own data. This is critical for any mobile app because the anon key is embedded in the app binary and can be extracted. RLS ensures that even with the key, users can only access their authorized data.
1-- Enable RLS2alter table public.todos enable row level security;34-- Users can read their own todos5create policy "Users can read own todos"6 on public.todos for select7 to authenticated8 using ((select auth.uid()) = user_id);910-- Users can create their own todos11create policy "Users can create own todos"12 on public.todos for insert13 to authenticated14 with check ((select auth.uid()) = user_id);1516-- Users can update their own todos17create policy "Users can update own todos"18 on public.todos for update19 to authenticated20 using ((select auth.uid()) = user_id);2122-- Users can delete their own todos23create policy "Users can delete own todos"24 on public.todos for delete25 to authenticated26 using ((select auth.uid()) = user_id);Expected result: Database access is secured at the row level. Authenticated users can only interact with their own data.
Complete working example
1import 'package:flutter/material.dart';2import 'package:supabase_flutter/supabase_flutter.dart';34void main() async {5 WidgetsFlutterBinding.ensureInitialized();67 await Supabase.initialize(8 url: 'https://your-project-ref.supabase.co',9 anonKey: 'your-anon-key-here',10 );1112 runApp(const MyApp());13}1415final supabase = Supabase.instance.client;1617class MyApp extends StatelessWidget {18 const MyApp({super.key});1920 @override21 Widget build(BuildContext context) {22 return MaterialApp(23 title: 'Supabase Flutter',24 home: StreamBuilder<AuthState>(25 stream: supabase.auth.onAuthStateChange,26 builder: (context, snapshot) {27 final session = snapshot.data?.session;28 if (session != null) {29 return const TodoListPage();30 }31 return const LoginPage();32 },33 ),34 );35 }36}3738class LoginPage extends StatefulWidget {39 const LoginPage({super.key});4041 @override42 State<LoginPage> createState() => _LoginPageState();43}4445class _LoginPageState extends State<LoginPage> {46 final emailController = TextEditingController();47 final passwordController = TextEditingController();4849 Future<void> signIn() async {50 try {51 await supabase.auth.signInWithPassword(52 email: emailController.text.trim(),53 password: passwordController.text,54 );55 } on AuthException catch (e) {56 if (mounted) {57 ScaffoldMessenger.of(context).showSnackBar(58 SnackBar(content: Text(e.message)),59 );60 }61 }62 }6364 @override65 Widget build(BuildContext context) {66 return Scaffold(67 appBar: AppBar(title: const Text('Login')),68 body: Padding(69 padding: const EdgeInsets.all(16),70 child: Column(children: [71 TextField(controller: emailController, decoration: const InputDecoration(labelText: 'Email')),72 TextField(controller: passwordController, obscureText: true, decoration: const InputDecoration(labelText: 'Password')),73 const SizedBox(height: 16),74 ElevatedButton(onPressed: signIn, child: const Text('Sign In')),75 ]),76 ),77 );78 }79}8081class TodoListPage extends StatefulWidget {82 const TodoListPage({super.key});8384 @override85 State<TodoListPage> createState() => _TodoListPageState();86}8788class _TodoListPageState extends State<TodoListPage> {89 List<Map<String, dynamic>> todos = [];9091 @override92 void initState() {93 super.initState();94 fetchTodos();95 }9697 Future<void> fetchTodos() async {98 final data = await supabase.from('todos').select().order('created_at');99 setState(() => todos = List<Map<String, dynamic>>.from(data));100 }101102 @override103 Widget build(BuildContext context) {104 return Scaffold(105 appBar: AppBar(106 title: const Text('My Todos'),107 actions: [108 IconButton(onPressed: () => supabase.auth.signOut(), icon: const Icon(Icons.logout)),109 ],110 ),111 body: ListView.builder(112 itemCount: todos.length,113 itemBuilder: (context, index) {114 final todo = todos[index];115 return ListTile(title: Text(todo['title'] ?? ''));116 },117 ),118 );119 }120}Common mistakes when connecting Supabase to Flutter
Why it's a problem: Calling Supabase.initialize() without WidgetsFlutterBinding.ensureInitialized() first, causing a runtime error
How to avoid: Always call WidgetsFlutterBinding.ensureInitialized() as the first line of main() before Supabase.initialize().
Why it's a problem: Hardcoding the service role key in the Flutter app, which gets bundled into the app binary
How to avoid: Only use the anon key in Flutter apps. The service role key bypasses all RLS and can be extracted from mobile app binaries.
Why it's a problem: Not enabling RLS on tables, leaving all data accessible to anyone with the anon key
How to avoid: Always enable RLS on every table and write policies. Mobile apps especially need RLS because the anon key is embedded in the app.
Why it's a problem: Using supabase_flutter 1.x initialization syntax with the 2.x package, causing API mismatch errors
How to avoid: Make sure you follow the 2.x documentation. Version 2 uses Supabase.initialize() instead of the old SupabaseFlutter.initialize() pattern.
Best practices
- Initialize Supabase once in main() and access it via Supabase.instance.client throughout the app
- Use StreamBuilder with onAuthStateChange for reactive navigation between auth and app screens
- Always enable RLS on all tables — mobile apps expose the API key in the binary
- Store Supabase credentials in a config file or use flutter_dotenv, not hardcoded strings
- Use try/catch with AuthException and PostgrestException for proper error handling
- Leverage the automatic session persistence — the Flutter SDK handles token refresh automatically
- Add indexes on columns used in RLS policies and query filters for better performance
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to build a Flutter app with Supabase as the backend. Show me how to initialize supabase_flutter, implement email/password auth, perform CRUD on a todos table, and handle auth state changes with StreamBuilder.
Help me connect my Flutter app to Supabase. I need the initialization code in main(), email/password auth methods, a todo list that queries the database, and RLS policies for the todos table.
Frequently asked questions
Does supabase_flutter handle session persistence automatically?
Yes. The Flutter SDK stores the session using shared preferences and automatically refreshes expired tokens. Users stay logged in between app restarts without any extra code.
Can I use Supabase with Flutter web?
Yes. supabase_flutter works on all Flutter platforms: Android, iOS, web, macOS, Windows, and Linux. The same code runs everywhere.
How do I handle deep links for OAuth in Flutter?
Configure the redirect URL in Supabase Dashboard and set up deep links in your Flutter app. The supabase_flutter package handles parsing the auth callback automatically when configured correctly.
Should I use the Supabase Dart client or supabase_flutter?
For Flutter apps, always use supabase_flutter. It wraps the Dart client and adds Flutter-specific features like automatic session persistence, deep link handling, and platform-aware initialization.
Can I use Supabase realtime subscriptions in Flutter?
Yes. Use supabase.channel() and .onPostgresChanges() to subscribe to database changes. The Flutter SDK maintains the WebSocket connection and delivers updates as Dart streams.
Can RapidDev help build a Flutter app with Supabase?
Yes. RapidDev can help you build cross-platform Flutter applications with Supabase, including auth flows, database design, realtime features, and deployment to app stores.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation