# How to Connect Supabase to Flutter

- Tool: Supabase
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Supabase (all plans), supabase_flutter 2.0+, Flutter 3.10+
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  supabase_flutter: ^2.0.0

# Then run:
flutter 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.

```
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    url: 'https://your-project-ref.supabase.co',
    anonKey: 'your-anon-key-here',
  );

  runApp(const MyApp());
}
```

**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.

```
final supabase = Supabase.instance.client;

// Sign up a new user
Future<void> signUp(String email, String password) async {
  final response = await supabase.auth.signUp(
    email: email,
    password: password,
  );
  if (response.user == null) {
    throw Exception('Sign up failed');
  }
}

// Sign in with email and password
Future<void> signIn(String email, String password) async {
  final response = await supabase.auth.signInWithPassword(
    email: email,
    password: password,
  );
  if (response.session == null) {
    throw Exception('Sign in failed');
  }
}

// Sign out
Future<void> signOut() async {
  await supabase.auth.signOut();
}
```

**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.

```
class AuthGate extends StatelessWidget {
  const AuthGate({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<AuthState>(
      stream: Supabase.instance.client.auth.onAuthStateChange,
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return const Center(child: CircularProgressIndicator());
        }

        final session = snapshot.data?.session;
        if (session != null) {
          return const HomePage();
        } else {
          return const LoginPage();
        }
      },
    );
  }
}
```

**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.

```
final supabase = Supabase.instance.client;

// Fetch all todos for the current user
Future<List<Map<String, dynamic>>> getTodos() async {
  final response = await supabase
      .from('todos')
      .select()
      .order('created_at', ascending: false);
  return List<Map<String, dynamic>>.from(response);
}

// Insert a new todo
Future<void> addTodo(String title) async {
  await supabase.from('todos').insert({
    'title': title,
    'is_complete': false,
    'user_id': supabase.auth.currentUser!.id,
  });
}

// Update a todo
Future<void> toggleTodo(int id, bool isComplete) async {
  await supabase
      .from('todos')
      .update({'is_complete': isComplete})
      .eq('id', id);
}

// Delete a todo
Future<void> deleteTodo(int id) async {
  await supabase.from('todos').delete().eq('id', id);
}
```

**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.

```
-- Enable RLS
alter table public.todos enable row level security;

-- Users can read their own todos
create policy "Users can read own todos"
  on public.todos for select
  to authenticated
  using ((select auth.uid()) = user_id);

-- Users can create their own todos
create policy "Users can create own todos"
  on public.todos for insert
  to authenticated
  with check ((select auth.uid()) = user_id);

-- Users can update their own todos
create policy "Users can update own todos"
  on public.todos for update
  to authenticated
  using ((select auth.uid()) = user_id);

-- Users can delete their own todos
create policy "Users can delete own todos"
  on public.todos for delete
  to authenticated
  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 code example

File: `main.dart`

```dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Supabase.initialize(
    url: 'https://your-project-ref.supabase.co',
    anonKey: 'your-anon-key-here',
  );

  runApp(const MyApp());
}

final supabase = Supabase.instance.client;

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Supabase Flutter',
      home: StreamBuilder<AuthState>(
        stream: supabase.auth.onAuthStateChange,
        builder: (context, snapshot) {
          final session = snapshot.data?.session;
          if (session != null) {
            return const TodoListPage();
          }
          return const LoginPage();
        },
      ),
    );
  }
}

class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  Future<void> signIn() async {
    try {
      await supabase.auth.signInWithPassword(
        email: emailController.text.trim(),
        password: passwordController.text,
      );
    } on AuthException catch (e) {
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(e.message)),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(children: [
          TextField(controller: emailController, decoration: const InputDecoration(labelText: 'Email')),
          TextField(controller: passwordController, obscureText: true, decoration: const InputDecoration(labelText: 'Password')),
          const SizedBox(height: 16),
          ElevatedButton(onPressed: signIn, child: const Text('Sign In')),
        ]),
      ),
    );
  }
}

class TodoListPage extends StatefulWidget {
  const TodoListPage({super.key});

  @override
  State<TodoListPage> createState() => _TodoListPageState();
}

class _TodoListPageState extends State<TodoListPage> {
  List<Map<String, dynamic>> todos = [];

  @override
  void initState() {
    super.initState();
    fetchTodos();
  }

  Future<void> fetchTodos() async {
    final data = await supabase.from('todos').select().order('created_at');
    setState(() => todos = List<Map<String, dynamic>>.from(data));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Todos'),
        actions: [
          IconButton(onPressed: () => supabase.auth.signOut(), icon: const Icon(Icons.logout)),
        ],
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          final todo = todos[index];
          return ListTile(title: Text(todo['title'] ?? ''));
        },
      ),
    );
  }
}
```

## Common mistakes

- **Calling Supabase.initialize() without WidgetsFlutterBinding.ensureInitialized() first, causing a runtime error** — undefined Fix: Always call WidgetsFlutterBinding.ensureInitialized() as the first line of main() before Supabase.initialize().
- **Hardcoding the service role key in the Flutter app, which gets bundled into the app binary** — undefined Fix: Only use the anon key in Flutter apps. The service role key bypasses all RLS and can be extracted from mobile app binaries.
- **Not enabling RLS on tables, leaving all data accessible to anyone with the anon key** — undefined Fix: Always enable RLS on every table and write policies. Mobile apps especially need RLS because the anon key is embedded in the app.
- **Using supabase_flutter 1.x initialization syntax with the 2.x package, causing API mismatch errors** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-connect-supabase-to-flutter
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-connect-supabase-to-flutter
