Supabase Auth Flutter: Secure Your App
Supabase Auth Flutter: Secure Your App
Hey guys! Ever wanted to build a cool app with Flutter and need a super robust authentication system without all the headache? Well, you’re in luck because today we’re diving deep into Supabase Auth with Flutter . This isn’t just about slapping on a login button; we’re talking about setting up secure, user-friendly authentication that will make your app shine. Supabase, as you probably know, is an open-source Firebase alternative, and its authentication features are top-notch. Integrating it with Flutter is surprisingly smooth, and it opens up a world of possibilities for managing user accounts, sessions, and access control. We’ll walk through everything from the initial setup to implementing different authentication methods like email/password, magic links, and even social logins. Get ready to level up your Flutter app’s security and user management game because we’re about to make authentication a breeze.
Table of Contents
Getting Started with Supabase and Flutter
First things first, guys, let’s get our environment set up so we can start building awesome things. To begin your journey with
Supabase Auth in Flutter
, you’ll need a Supabase project. If you haven’t already, head over to
supabase.io
and create a free account. Once you’re logged in, create a new project. You’ll be greeted with a dashboard where you’ll find your project URL and your
anon
public key. Keep these handy; they’re like your app’s secret handshake with Supabase. Now, for your Flutter project, you’ll need to add the
supabase_flutter
package. Open your
pubspec.yaml
file and add this line under
dependencies
:
supabase_flutter: ^1.10.6
After saving the file, run
flutter pub get
in your terminal. Next, you need to initialize the Supabase client in your
main.dart
file. This is crucial for your app to communicate with your Supabase backend. You’ll use the project URL and
anon
key you got earlier. Here’s a snippet of how you might do it:
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_SUPABASE_ANON_KEY',
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Supabase Auth Demo',
home: AuthPage(), // We'll create this page soon!
);
}
}
Remember to replace
'YOUR_SUPABASE_URL'
and
'YOUR_SUPABASE_ANON_KEY'
with your actual Supabase project credentials. This setup is the foundation for everything we’ll do with Supabase Auth in Flutter. It ensures your Flutter app is connected and ready to authenticate users.
Setting up Supabase and Flutter
correctly at this stage will save you a lot of trouble down the line, so double-check those keys!
Implementing Email and Password Authentication
Alright, team, let’s get down to the nitty-gritty of
implementing email and password authentication in Flutter using Supabase
. This is a classic method and a great starting point for most applications. Supabase makes this incredibly straightforward. We’ll need a UI for users to sign up and sign in. Let’s create a simple
AuthPage
widget. This page will handle the display of sign-up and sign-in forms and the logic to interact with Supabase.
First, create a new file, say
auth_page.dart
, and put this basic structure in it:
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
class AuthPage extends StatefulWidget {
const AuthPage({Key? key}) : super(key: key);
@override
_AuthPageState createState() => _AuthPageState();
}
class _AuthPageState extends State<AuthPage> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isSigningUp = true;
Future<void> _submit() async {
final isValid = _formKey.currentState?.validate() ?? false;
if (!isValid) {
return;
}
_formKey.currentState?.save();
try {
if (_isSigningUp) {
// Sign up logic
await Supabase.instance.client.auth.signUp(
email: _emailController.text,
password: _passwordController.text,
);
// Optionally navigate to a success page or show a message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sign up successful! Please check your email.')),
);
} else {
// Sign in logic
await Supabase.instance.client.auth.signInWithPassword(
email: _emailController.text,
password: _passwordController.text,
);
// On successful sign-in, you'd typically navigate to your app's main screen
Navigator.of(context).pushReplacementNamed('/home'); // Assuming you have a '/home' route
}
} on AuthException catch (error) {
// Handle authentication errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Authentication error: ${error.message}')),
);
} catch (error) {
// Handle other potential errors
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An unexpected error occurred: $error')),
);
}
}
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(_isSigningUp ? 'Sign Up' : 'Sign In')),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !value.contains('@')) {
return 'Please enter a valid email.';
}
return null;
},
),
SizedBox(height: 10),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: (value) {
if (value == null || value.length < 6) {
return 'Password must be at least 6 characters long.';
}
return null;
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submit,
child: Text(_isSigningUp ? 'Sign Up' : 'Sign In'),
),
TextButton(
onPressed: () {
setState(() {
_isSigningUp = !_isSigningUp;
});
},
child: Text(_isSigningUp ? 'Already have an account? Sign In' : 'Don\'t have an account? Sign Up'),
),
],
),
),
),
),
);
}
}
In this code, we have two text fields for email and password, a submit button, and a toggle button to switch between signing up and signing in. The
_submit
function handles the logic. If
_isSigningUp
is true, it calls
auth.signUp()
; otherwise, it calls
auth.signInWithPassword()
. We also include basic validation and error handling using
try-catch
blocks.
Implementing email and password authentication
this way gives you a solid foundation for user management in your Flutter app. Remember that for sign-up, Supabase, by default, sends a confirmation email. You’ll need to configure email templates in your Supabase project settings for a better user experience. This is a critical step for
secure user authentication in Flutter
.
Using Magic Links for Passwordless Authentication
Hey coders, let’s spice things up and talk about a super cool way to authenticate your users without them even needing to remember a password: magic links with Supabase Auth in Flutter ! This method is fantastic for user experience because it’s simple and secure. A magic link is essentially a special URL sent to the user’s email. When they click it, it authenticates them automatically. Supabase makes this a piece of cake to implement.
First, we need to enable email auth and magic links in your Supabase project settings. Navigate to your Supabase dashboard, go to Authentication -> Settings, and ensure ‘Email auth providers’ has ‘Email’ enabled. Then, under ‘Auth Providers’, enable ‘Email OTP’ and configure your ‘Site URL’. This URL is where Supabase will redirect the user after they click the magic link. Make sure it matches the URL you’ve configured in your Flutter app’s
AndroidManifest.xml
(for Android) or
Info.plist
(for iOS) to handle the deep link.
Now, let’s modify our
AuthPage
or create a new widget specifically for magic links. We’ll need an email input field. The process is: user enters email, clicks a button, Supabase sends the magic link, and the user clicks it to log in.
Here’s how you can integrate the magic link sign-in logic. You can add a new button or modify the existing submit button’s behavior. Let’s assume you want a separate flow for magic links. We’ll add a function
_signInWithMagicLink()
:
Future<void> _signInWithMagicLink() async {
final isValid = _formKey.currentState?.validate() ?? false;
if (!isValid) {
return;
}
_formKey.currentState?.save();
try {
await Supabase.instance.client.auth.signInWithOtp(
email: _emailController.text,
emailRedirectTo: Supabase.instance.client.auth.settings.redirectUrl,
// For mobile apps, you might need to specify a mobileRedirectTo
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Magic link sent! Check your email.')),
);
// Optionally navigate to a pending verification page
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error sending magic link: ${error.message}')),
);
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An unexpected error occurred: $error')),
);
}
}
You would then call this
_signInWithMagicLink
function from a button specifically designed for this purpose. The user enters their email, hits the button, and gets the magic link. When they click that link, Supabase handles the authentication. For this to work seamlessly, you need to set up deep linking in your Flutter app. This involves configuring your Android and iOS projects to recognize the URL scheme that Supabase uses for magic links. This
passwordless authentication in Flutter
provides a streamlined user experience. It’s a fantastic alternative to traditional username and password systems and is a key feature when discussing
Supabase Auth with Flutter
.
Handling User Sessions and State Management
Now that we’ve got users signing up and signing in, the next crucial piece of the puzzle is managing user sessions and state in Flutter with Supabase Auth . You don’t want users to have to log in every single time they open your app, right? Supabase handles session management for you, but you need to build the logic in your Flutter app to react to the user’s authentication state. This means knowing when a user is logged in, logged out, or if their session has expired.
The
supabase_flutter
package provides a stream that listens to authentication state changes. This stream,
Supabase.instance.client.auth.onAuthStateChange
, is your best friend here. It emits events whenever the user’s authentication status changes, such as when they sign in, sign out, or their token is refreshed.
We can leverage this stream to automatically redirect users to the appropriate screen. A common pattern is to have a root widget that listens to this stream and decides whether to show the authentication pages or the main application content. Let’s create a
SplashPage
or
RootPage
that handles this:
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'auth_page.dart'; // Assuming AuthPage is in auth_page.dart
class RootPage extends StatefulWidget {
const RootPage({Key? key}) : super(key: key);
@override
_RootPageState createState() => _RootPageState();
}
class _RootPageState extends State<RootPage> {
User? _user;
@override
void initState() {
super.initState();
// Get the current user initially
_user = Supabase.instance.client.auth.currentUser;
// Listen for authentication state changes
Supabase.instance.client.auth.onAuthStateChange.listen((data) {
final AuthChangeEvent eventType = data.event;
final Session? session = data.session;
setState(() {
_user = session?.user;
});
// You can add logic here to navigate based on eventType if needed
// For example, if eventType == AuthChangeEvent.signedOut, navigate to AuthPage
});
}
@override
Widget build(BuildContext context) {
return _user == null ? AuthPage() : HomePage(); // HomePage needs to be defined
}
}
// Dummy HomePage for demonstration
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final user = Supabase.instance.client.auth.currentUser;
return Scaffold(
appBar: AppBar(title: Text('Welcome!'), actions: [
IconButton(onPressed: () async {
await Supabase.instance.client.auth.signOut();
// The listener in RootPage will handle navigation back to AuthPage
}, icon: Icon(Icons.logout))
]),
body: Center(
child: Text('Hello, ${user?.email ?? 'User'}!'),
),
);
}
}
In this example,
RootPage
checks for the current user (
Supabase.instance.client.auth.currentUser
). If no user is logged in, it shows
AuthPage
; otherwise, it shows
HomePage
. The
onAuthStateChange
stream is used to update the
_user
state whenever it changes. This ensures your UI always reflects the current authentication status. When the user logs out, the stream detects it,
_user
becomes null, and
RootPage
automatically displays
AuthPage
again.
Managing user sessions in Flutter
this way is crucial for a seamless user experience and is a core aspect of
Supabase Auth for Flutter
applications. This robust state management ensures your app remains secure and user-friendly.
Advanced Features: Social Logins and Row Level Security
Beyond the basics, Supabase Auth with Flutter offers powerful features like social logins and Row Level Security (RLS) that can significantly enhance your app. Social logins, such as Google, GitHub, or Facebook, provide users with familiar and convenient ways to sign up and log in, reducing friction and increasing adoption. Supabase makes integrating these providers relatively simple.
To enable social logins, you first need to configure them in your Supabase project settings under Authentication -> Providers. For each provider (e.g., Google), you’ll need to create an application in the provider’s developer console and obtain API keys (Client ID and Client Secret). You then enter these credentials into your Supabase project settings. Once configured, Supabase handles the OAuth flow for you.
In your Flutter app, you can initiate a social login using the
auth.signInWithProvider()
method. For example, to sign in with Google:
Future<void> _signInWithGoogle() async {
try {
await Supabase.instance.client.auth.signInWithProvider(
OAuthProvider.google,
);
// The onAuthStateChange stream will handle session updates and navigation
} on AuthException catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Google Sign-In error: ${error.message}')),
);
} catch (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('An unexpected error occurred: $error')),
);
}
}
Supabase’s
onAuthStateChange
listener will automatically pick up the successful login, and your session management logic (as discussed earlier) will take over. This makes
integrating social logins in Flutter
feel almost as easy as email/password authentication.
Now, let’s talk about Row Level Security (RLS) . This is where Supabase truly shines in terms of security. RLS allows you to define fine-grained access control policies directly within your database. Instead of relying solely on backend code to check permissions, you write policies that determine whether a user can read, write, or delete specific rows in your tables. This is incredibly powerful for building secure, data-driven applications.
For example, imagine you have a
todos
table. You might want to ensure that a user can only access their own todos. You can write an RLS policy like this (in SQL within Supabase SQL Editor):
-- Enable RLS for the todos table
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Policy for selecting todos
CREATE POLICY "Users can select their own todos" ON todos
FOR SELECT USING (auth.uid() = user_id);
-- Policy for inserting todos
CREATE POLICY "Users can insert their own todos" ON todos
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Policy for updating todos
CREATE POLICY "Users can update their own todos" ON todos
FOR UPDATE USING (auth.uid() = user_id);
-- Policy for deleting todos
CREATE POLICY "Users can delete their own todos" ON todos
FOR DELETE USING (auth.uid() = user_id);
In these policies,
auth.uid()
is a Supabase function that returns the ID of the currently authenticated user. By comparing this to the
user_id
column in the
todos
table, we ensure that only the owner of a todo can interact with it.
Implementing RLS in Flutter
means that your API calls will automatically be checked against these database policies, providing a robust security layer. This combination of easy-to-use authentication and powerful RLS makes
Supabase Auth with Flutter
an excellent choice for building secure and scalable applications.
Conclusion: Mastering Supabase Auth in Flutter
So there you have it, folks! We’ve covered the essentials and some advanced techniques for
Supabase Auth in Flutter
. From the initial setup and basic email/password authentication to implementing slick passwordless magic links, and even diving into social logins and the powerhouse of Row Level Security, you’ve got a solid toolkit.
Supabase Auth for Flutter
offers a flexible, secure, and developer-friendly way to manage users in your applications. The
supabase_flutter
package makes integrating these features a joy, and the Supabase platform itself provides a robust backend that scales with your needs.
Remember the key takeaways:
secure your Flutter app
with proper authentication flows, leverage
passwordless authentication
for better UX, manage user sessions diligently using the
onAuthStateChange
stream, and don’t shy away from
advanced features
like social logins and RLS to build truly secure and sophisticated applications. Supabase is constantly evolving, so keeping an eye on their documentation is always a good idea. By mastering
Supabase Auth with Flutter
, you’re well on your way to building production-ready applications with confidence. Happy coding, everyone!