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.