Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Connect Supabase to Flutter

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.

What you'll learn

  • How to install and initialize supabase_flutter in a Flutter project
  • How to perform authentication with email/password in Dart
  • How to query and insert data using the Supabase client in Flutter
  • How to handle session persistence and auth state changes
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15-20 minSupabase (all plans), supabase_flutter 2.0+, Flutter 3.10+March 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1# pubspec.yaml
2dependencies:
3 flutter:
4 sdk: flutter
5 supabase_flutter: ^2.0.0
6
7# Then run:
8flutter pub get

Expected result: The supabase_flutter package is installed and available for import in your Dart code.

2

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.

typescript
1import 'package:flutter/material.dart';
2import 'package:supabase_flutter/supabase_flutter.dart';
3
4void main() async {
5 WidgetsFlutterBinding.ensureInitialized();
6
7 await Supabase.initialize(
8 url: 'https://your-project-ref.supabase.co',
9 anonKey: 'your-anon-key-here',
10 );
11
12 runApp(const MyApp());
13}

Expected result: Supabase is initialized and the client is accessible via Supabase.instance.client anywhere in the app.

3

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.

typescript
1final supabase = Supabase.instance.client;
2
3// Sign up a new user
4Future<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}
13
14// Sign in with email and password
15Future<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}
24
25// Sign out
26Future<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.

4

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.

typescript
1class AuthGate extends StatelessWidget {
2 const AuthGate({super.key});
3
4 @override
5 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 }
12
13 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.

5

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.

typescript
1final supabase = Supabase.instance.client;
2
3// Fetch all todos for the current user
4Future<List<Map<String, dynamic>>> getTodos() async {
5 final response = await supabase
6 .from('todos')
7 .select()
8 .order('created_at', ascending: false);
9 return List<Map<String, dynamic>>.from(response);
10}
11
12// Insert a new todo
13Future<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}
20
21// Update a todo
22Future<void> toggleTodo(int id, bool isComplete) async {
23 await supabase
24 .from('todos')
25 .update({'is_complete': isComplete})
26 .eq('id', id);
27}
28
29// Delete a todo
30Future<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.

6

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.

typescript
1-- Enable RLS
2alter table public.todos enable row level security;
3
4-- Users can read their own todos
5create policy "Users can read own todos"
6 on public.todos for select
7 to authenticated
8 using ((select auth.uid()) = user_id);
9
10-- Users can create their own todos
11create policy "Users can create own todos"
12 on public.todos for insert
13 to authenticated
14 with check ((select auth.uid()) = user_id);
15
16-- Users can update their own todos
17create policy "Users can update own todos"
18 on public.todos for update
19 to authenticated
20 using ((select auth.uid()) = user_id);
21
22-- Users can delete their own todos
23create policy "Users can delete own todos"
24 on public.todos for delete
25 to authenticated
26 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

main.dart
1import 'package:flutter/material.dart';
2import 'package:supabase_flutter/supabase_flutter.dart';
3
4void main() async {
5 WidgetsFlutterBinding.ensureInitialized();
6
7 await Supabase.initialize(
8 url: 'https://your-project-ref.supabase.co',
9 anonKey: 'your-anon-key-here',
10 );
11
12 runApp(const MyApp());
13}
14
15final supabase = Supabase.instance.client;
16
17class MyApp extends StatelessWidget {
18 const MyApp({super.key});
19
20 @override
21 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}
37
38class LoginPage extends StatefulWidget {
39 const LoginPage({super.key});
40
41 @override
42 State<LoginPage> createState() => _LoginPageState();
43}
44
45class _LoginPageState extends State<LoginPage> {
46 final emailController = TextEditingController();
47 final passwordController = TextEditingController();
48
49 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 }
63
64 @override
65 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}
80
81class TodoListPage extends StatefulWidget {
82 const TodoListPage({super.key});
83
84 @override
85 State<TodoListPage> createState() => _TodoListPageState();
86}
87
88class _TodoListPageState extends State<TodoListPage> {
89 List<Map<String, dynamic>> todos = [];
90
91 @override
92 void initState() {
93 super.initState();
94 fetchTodos();
95 }
96
97 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 }
101
102 @override
103 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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.