Authentication
Implementing email/password login, signup, session management, and OAuth providers with Supabase.
Supabase provides a complete authentication system that supports:
- Email/Password – Traditional email and password login
- Magic Links – Passwordless login via email
- OAuth Providers – Google, GitHub, Apple, Facebook, and more
- Session Management – Automatic session handling with refresh tokens
- Password Reset – Secure password reset flow
- User Profiles – Store user data in PostgreSQL tables
🔐 Supabase Auth vs Firebase Auth
- Same features – Email, OAuth, magic links, password reset
-
PostgreSQL integration
– User data stored in
auth.userstable - RLS integration – Row Level Security policies work with auth
- Simpler pricing – 50,000 monthly active users on free tier
- Open source – Can self-host if needed
To use authentication in your Flutter app, you need to configure your Supabase project and initialize the client.
dependencies:
supabase_flutter: ^2.8.0
import
'package:flutter/material.dart'
;
import
'package:supabase_flutter/supabase_flutter.dart'
;
void
main
() async {
await
Supabase.initialize(
url:
'https://your-project.supabase.co'
,
anonKey:
'your-anon-key'
,
// Optional: Set up auth redirects for deep linking
authOptions: FlutterAuthClientOptions(
authFlowType: AuthFlowType.pkce,
),
);
runApp(
MyApp
());
}
💡 Auth Flow Types
- pkce – Proof Key for Code Exchange (recommended for mobile apps)
- implicit – Implicit flow (simpler but less secure)
To create a new user account, use the
signUp()
method with email and password.
import
'package:supabase_flutter/supabase_flutter.dart'
;
class
AuthService
{
final
SupabaseClient client = Supabase.instance.client;
Future
<AuthResponse>
signUp
({
required
String email,
required
String password,
String
? username,
}) async {
try
{
final
response =
await
client.auth.signUp(
email: email,
password: password,
data: {
'username'
: username ?? email.split(
'@'
)[
0
],
'created_at'
: DateTime.now().toIso8601String(),
},
);
return
response;
}
catch
(e) {
throw
Exception(
'Sign up failed: $e'
);
}
}
}
class
SignUpScreen
extends
StatefulWidget {
@override
State<SignUpScreen>
createState
() => _SignUpScreenState();
}
class
_SignUpScreenState
extends
State<SignUpScreen> {
final
TextEditingController _emailController = TextEditingController();
final
TextEditingController _passwordController = TextEditingController();
final
TextEditingController _usernameController = TextEditingController();
final
AuthService _authService = AuthService();
bool
_isLoading =
false
;
String? _errorMessage;
Future
<
void
>
_signUp
() async {
final
email = _emailController.text.trim();
final
password = _passwordController.text.trim();
final
username = _usernameController.text.trim();
if
(email.isEmpty || password.isEmpty) {
setState(() => _errorMessage =
'Please fill in all fields'
);
return
;
}
if
(password.length <
6
) {
setState(() => _errorMessage =
'Password must be at least 6 characters'
);
return
;
}
setState(() {
_isLoading =
true
;
_errorMessage =
null
;
});
try
{
final
response =
await
_authService.signUp(
email: email,
password: password,
username: username.isNotEmpty ? username :
null
,
);
if
(response.user !=
null
) {
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Account created! Please check your email for verification.'
),
backgroundColor: Colors.green,
),
);
Navigator.pop(context);
}
}
catch
(e) {
setState(() {
_isLoading =
false
;
_errorMessage = e.toString().replaceAll(
'Exception: '
,
''
);
});
}
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(title: Text(
'Sign Up'
)),
body: Padding(
padding: EdgeInsets.all(
24
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText:
'Email'
,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
autocorrect:
false
,
),
SizedBox(height:
16
),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText:
'Password'
,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
),
obscureText:
true
,
),
SizedBox(height:
16
),
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText:
'Username (optional)'
,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person),
),
),
if
(_errorMessage !=
null
)
Padding(
padding: EdgeInsets.only(top:
16
),
child: Text(
_errorMessage!,
style: TextStyle(color: Colors.red),
),
),
SizedBox(height:
24
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ?
null
: _signUp,
child: _isLoading
? CircularProgressIndicator(
strokeWidth:
2
,
color: Colors.white,
)
: Text(
'Sign Up'
),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical:
16
),
),
),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Already have an account? Log in'
),
),
],
),
),
);
}
}
To log in an existing user, use the
signInWithPassword()
method.
// Add to AuthService class
Future
<AuthResponse>
signIn
({
required
String email,
required
String password,
}) async {
try
{
final
response =
await
client.auth.signInWithPassword(
email: email,
password: password,
);
return
response;
}
catch
(e) {
throw
Exception(
'Sign in failed: $e'
);
}
}
Future
<
void
>
signOut
() async {
try
{
await
client.auth.signOut();
}
catch
(e) {
throw
Exception(
'Sign out failed: $e'
);
}
}
Future
<User?>
getCurrentUser
() {
return
Future.value(client.auth.currentSession?.user);
}
bool
isLoggedIn
() {
return
client.auth.currentSession !=
null
;
}
class
LoginScreen
extends
StatefulWidget {
@override
State<LoginScreen>
createState
() => _LoginScreenState();
}
class
_LoginScreenState
extends
State<LoginScreen> {
final
TextEditingController _emailController = TextEditingController();
final
TextEditingController _passwordController = TextEditingController();
final
AuthService _authService = AuthService();
bool
_isLoading =
false
;
String? _errorMessage;
bool
_obscurePassword =
true
;
Future
<
void
>
_signIn
() async {
final
email = _emailController.text.trim();
final
password = _passwordController.text.trim();
if
(email.isEmpty || password.isEmpty) {
setState(() => _errorMessage =
'Please enter your email and password'
);
return
;
}
setState(() {
_isLoading =
true
;
_errorMessage =
null
;
});
try
{
final
response =
await
_authService.signIn(
email: email,
password: password,
);
if
(response.user !=
null
) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => HomeScreen()),
);
}
}
catch
(e) {
setState(() {
_isLoading =
false
;
_errorMessage = e.toString().replaceAll(
'Exception: '
,
''
);
});
}
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(title: Text(
'Log In'
)),
body: Padding(
padding: EdgeInsets.all(
24
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Welcome Back!'
,
style: TextStyle(
fontSize:
24
,
fontWeight: FontWeight.bold,
),
),
SizedBox(height:
8
),
Text(
'Sign in to continue'
,
style: TextStyle(color: Colors.grey.shade600),
),
SizedBox(height:
32
),
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText:
'Email'
,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
autocorrect:
false
,
),
SizedBox(height:
16
),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText:
'Password'
,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
obscureText: _obscurePassword,
onSubmitted: (_) => _signIn(),
),
if
(_errorMessage !=
null
)
Padding(
padding: EdgeInsets.only(top:
16
),
child: Text(
_errorMessage!,
style: TextStyle(color: Colors.red),
),
),
SizedBox(height:
24
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ?
null
: _signIn,
child: _isLoading
? CircularProgressIndicator(
strokeWidth:
2
,
color: Colors.white,
)
: Text(
'Log In'
),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical:
16
),
),
),
),
TextButton(
onPressed: () {
// Navigate to sign up
},
child: Text(
'Don\'t have an account? Sign up'
),
),
TextButton(
onPressed: () {
// Navigate to password reset
},
child: Text(
'Forgot password?'
,
style: TextStyle(color: Colors.grey.shade600),
),
),
],
),
),
);
}
}
Magic links allow users to log in without a password by sending a one-time link to their email.
Future
<
void
>
sendMagicLink
(String email) async {
try
{
await
client.auth.signInWithOtp(
email: email,
emailRedirectTo:
'com.example.app://auth-callback'
,
);
}
catch
(e) {
throw
Exception(
'Failed to send magic link: $e'
);
}
}
// Handle deep link for magic link callback
void
handleMagicLink
(Uri uri) async {
try
{
final
session =
await
client.auth.getSessionFromUrl(uri);
if
(session !=
null
) {
// User is now logged in
}
}
catch
(e) {
print(
'Error handling magic link: $e'
);
}
}
class
MagicLinkScreen
extends
StatefulWidget {
@override
State<MagicLinkScreen>
createState
() => _MagicLinkScreenState();
}
class
_MagicLinkScreenState
extends
State<MagicLinkScreen> {
final
TextEditingController _emailController = TextEditingController();
final
AuthService _authService = AuthService();
bool
_isLoading =
false
;
bool
_isSent =
false
;
String? _errorMessage;
Future
<
void
>
_sendMagicLink
() async {
final
email = _emailController.text.trim();
if
(email.isEmpty) {
setState(() => _errorMessage =
'Please enter your email'
);
return
;
}
setState(() {
_isLoading =
true
;
_errorMessage =
null
;
});
try
{
await
_authService.sendMagicLink(email);
setState(() {
_isLoading =
false
;
_isSent =
true
;
});
}
catch
(e) {
setState(() {
_isLoading =
false
;
_errorMessage = e.toString().replaceAll(
'Exception: '
,
''
);
});
}
}
@override
Widget
build
(BuildContext context) {
return
Scaffold(
appBar: AppBar(title: Text(
'Magic Link'
)),
body: Padding(
padding: EdgeInsets.all(
24
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'🔗'
,
style: TextStyle(fontSize:
48
),
),
SizedBox(height:
16
),
Text(
'Get a Magic Link'
,
style: TextStyle(fontSize:
24
, fontWeight: FontWeight.bold),
),
SizedBox(height:
8
),
Text(
'We\'ll send you a one-time link to log in instantly'
,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade600),
),
SizedBox(height:
32
),
if
(_isSent)
Column(
children: [
Icon(Icons.check_circle, color: Colors.green, size:
64
),
SizedBox(height:
16
),
Text(
'Check your email!'
,
style: TextStyle(fontSize:
18
, fontWeight: FontWeight.bold),
),
Text(
'Click the link in your email to log in'
,
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey.shade600),
),
SizedBox(height:
16
),
TextButton(
onPressed: () {
setState(() => _isSent =
false
);
},
child: Text(
'Send again'
),
),
],
)
else
Column(
children: [
TextField(
controller: _emailController,
decoration: InputDecoration(
labelText:
'Email Address'
,
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.email),
),
keyboardType: TextInputType.emailAddress,
autocorrect:
false
,
),
if
(_errorMessage !=
null
)
Padding(
padding: EdgeInsets.only(top:
16
),
child: Text(
_errorMessage!,
style: TextStyle(color: Colors.red),
),
),
SizedBox(height:
24
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ?
null
: _sendMagicLink,
child: _isLoading
? CircularProgressIndicator(
strokeWidth:
2
,
color: Colors.white,
)
: Text(
'Send Magic Link'
),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical:
16
),
),
),
),
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Back to log in'
),
),
],
),
],
),
),
);
}
}
Supabase supports multiple OAuth providers for social login:
Future
<AuthResponse>
signInWithGoogle
() async {
try
{
return
await
client.auth.signInWithOAuth(
OAuthProvider.google,
redirectTo:
'com.example.app://auth-callback'
,
);
}
catch
(e) {
throw
Exception(
'Google sign in failed: $e'
);
}
}
Future
<AuthResponse>
signInWithGitHub
() async {
try
{
return
await
client.auth.signInWithOAuth(
OAuthProvider.github,
redirectTo:
'com.example.app://auth-callback'
,
);
}
catch
(e) {
throw
Exception(
'GitHub sign in failed: $e'
);
}
}
Future
<AuthResponse>
signInWithApple
() async {
try
{
return
await
client.auth.signInWithOAuth(
OAuthProvider.apple,
redirectTo:
'com.example.app://auth-callback'
,
);
}
catch
(e) {
throw
Exception(
'Apple sign in failed: $e'
);
}
}
⚠️ OAuth Setup Required
- Configure OAuth providers in Supabase Dashboard → Authentication → Providers
-
Add
redirect URIs
for your app (e.g.,
com.example.app://auth-callback) -
For Android: Add
intent-filterinAndroidManifest.xml -
For iOS: Add
URL TypesinInfo.plist - For web: Add the redirect URL in your Supabase project settings
To allow users to reset their password, use the
resetPasswordForEmail()
method.
Future
<
void
>
resetPassword
(String email) async {
try
{
await
client.auth.resetPasswordForEmail(
email,
redirectTo:
'com.example.app://reset-password'
,
);
}
catch
(e) {
throw
Exception(
'Password reset failed: $e'
);
}
}
Future
<
void
>
updatePassword
(String newPassword) async {
try
{
await
client.auth.updateUser(
UserAttributes(password: newPassword),
);
}
catch
(e) {
throw
Exception(
'Password update failed: $e'
);
}
}
Supabase handles session management automatically with refresh tokens. Here's how to check the auth state.
import
'package:flutter_riverpod/flutter_riverpod.dart'
;
import
'package:supabase_flutter/supabase_flutter.dart'
;
final
authStateProvider = StreamProvider<AuthState>((ref) {
return
Supabase.instance.client.auth.onAuthStateChange.map((event) {
return
event.data;
});
});
final
currentUserProvider = Provider<User?>((ref) {
return
Supabase.instance.client.auth.currentSession?.user;
});
final
isLoggedInProvider = Provider<
bool
>((ref) {
return
Supabase.instance.client.auth.currentSession !=
null
;
});
class
AuthGuard
extends
ConsumerWidget {
final
Widget child;
const
AuthGuard({
required
this
.child, super.key});
@override
Widget
build
(BuildContext context, WidgetRef ref) {
final
isLoggedIn = ref.watch(isLoggedInProvider);
if
(isLoggedIn) {
return
child;
}
// Redirect to login
return
LoginScreen();
}
}
supabase_flutter
package and initialize the client with your project credentials.
signUp()
with email and password, storing additional user data in the
data
parameter.
signInWithPassword()
to log in existing users.
signInWithOtp()
for passwordless login via email.
signInWithOAuth()
for social login with Google, GitHub, Apple, etc.
resetPasswordForEmail()
and
updateUser()
for password changes.
onAuthStateChange
.
By default, Supabase requires email verification. Users must verify their email before they can log in.
Show a verification message after sign up. Use the
confirmEmail()
method to handle the verification link.
OAuth and magic link redirects must match exactly what's configured in your Supabase dashboard.
For mobile apps, use a custom URL scheme like
com.example.app://auth-callback
.
Sessions expire after a certain time. If you don't handle this, users will see errors.
Listen to auth state changes to react to session expiration and update your UI accordingly.
🎯 Key Takeaway
Supabase provides a complete authentication system with email/password, magic links, and OAuth providers. The session management is automatic with refresh tokens. Always handle verification flows and use Row Level Security to protect user data.