Flutter + Supabase Authentication: Complete Guide to Sign in with Google and Apple
A comprehensive step-by-step guide to implementing Google and Apple Sign-In in your Flutter app using Supabase. Learn how to configure OAuth providers, handle platform-specific setup, and create a seamless authentication experience.

Flutter + Supabase Authentication: Complete Guide to Sign in with Google and Apple
TL;DR
- Supabase provides a simple way to add OAuth authentication to your Flutter app
- Google Sign-In works on both iOS and Android
- Apple Sign-In is required for iOS apps that offer social login (App Store guideline)
- This guide covers Google Cloud Console, Apple Developer setup, Supabase configuration, and Flutter implementation
- Total setup time: ~1-2 hours
Social authentication has become the standard for mobile apps. Users expect to sign in with their existing Google or Apple accounts rather than creating yet another username and password. In this guide, I'll walk you through implementing both providers using Supabase as your backend.
Why Supabase for Authentication?
Before diving into the implementation, let's understand why Supabase is an excellent choice:
- Open Source - No vendor lock-in, you can self-host if needed
- Generous Free Tier - 50,000 monthly active users for free
- Built-in User Management - Database tables, row-level security, and user metadata
- Multiple Auth Providers - Google, Apple, GitHub, Twitter, and more
- Flutter SDK - First-class support with type-safe client
Prerequisites
Before starting, ensure you have:
- A Flutter project set up (Flutter 3.0+)
- A Supabase account (free at supabase.com)
- Access to Google Cloud Console
- Access to Apple Developer account (for Apple Sign-In)
- Your app's bundle ID/package name ready
Part 1: Supabase Project Setup
Step 1.1: Create a Supabase Project
- Go to Supabase Dashboard
- Click New Project
- Fill in:
- Name: Your project name (e.g., "MyApp")
- Database Password: Generate a strong password (save this!)
- Region: Choose closest to your users
- Click Create new project
- Wait 2-3 minutes for setup to complete
Step 1.2: Get Your Project Credentials
Once your project is ready:
- Go to Project Settings (gear icon)
- Click API in the sidebar
- Note down:
- Project URL:
https://xxxxxxxxxxxx.supabase.co - anon public key:
eyJhbGciOiJIUzI1NiIs...
- Project URL:
Security Note: The anon key is safe to include in your app. Row Level Security (RLS) policies protect your data.
Step 1.3: Configure Authentication Settings
- Go to Authentication in the sidebar
- Click Providers
- You'll see a list of available providers - we'll configure Google and Apple next
Part 2: Google Sign-In Setup
Step 2.1: Create Google Cloud Project
- Go to Google Cloud Console
- Click the project dropdown at the top → New Project
- Name your project and click Create
- Select your new project from the dropdown
Step 2.2: Configure OAuth Consent Screen
- Go to APIs & Services → OAuth consent screen
- Select External (unless you have Google Workspace)
- Click Create
- Fill in required fields:
- App name: Your app name
- User support email: Your email
- Developer contact information: Your email
- Click Save and Continue
- Skip Scopes for now → Click Save and Continue
- Add test users if needed → Click Save and Continue
- Review and click Back to Dashboard
Step 2.3: Create OAuth Credentials
For Web (Required for Supabase)
- Go to APIs & Services → Credentials
- Click Create Credentials → OAuth client ID
- Select Web application
- Name it (e.g., "Supabase Web Client")
- Under Authorized redirect URIs, add:
(Replacehttps://YOUR_PROJECT_REF.supabase.co/auth/v1/callbackYOUR_PROJECT_REFwith your Supabase project reference) - Click Create
- Copy the Client ID and Client Secret
For iOS
- Click Create Credentials → OAuth client ID
- Select iOS
- Enter your Bundle ID (e.g.,
com.yourcompany.yourapp) - Click Create
- Copy the iOS Client ID (you'll need this in your Flutter app)
For Android
- Click Create Credentials → OAuth client ID
- Select Android
- Enter:
- Package name: Your Android package (e.g.,
com.yourcompany.yourapp) - SHA-1 certificate fingerprint: Get this by running:
# For debug keystore keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android # For release keystore keytool -list -v -keystore /path/to/release.keystore -alias your-alias
- Package name: Your Android package (e.g.,
- Click Create
Step 2.4: Configure Google in Supabase
- Go to your Supabase Dashboard → Authentication → Providers
- Find Google and click to expand
- Toggle Enable Sign in with Google
- Enter:
- Client ID: Web Client ID from Step 2.3
- Client Secret: Web Client Secret from Step 2.3
- Click Save
Part 3: Apple Sign-In Setup
Important: Apple Sign-In is required by App Store guidelines if your iOS app offers any other social login option.
Step 3.1: Configure App ID
- Go to Apple Developer Portal
- Navigate to Certificates, Identifiers & Profiles
- Click Identifiers → Select your App ID (or create one)
- Scroll to Sign In with Apple
- Check the checkbox to enable
- Click Edit next to Sign In with Apple
- Select Enable as a primary App ID
- Click Save
Step 3.2: Create a Service ID
- Go to Identifiers → Click + to add new
- Select Services IDs → Click Continue
- Fill in:
- Description: Your app name (e.g., "MyApp Web")
- Identifier: Reverse domain (e.g.,
com.yourcompany.yourapp.web)
- Click Continue → Register
- Click on your newly created Service ID
- Check Sign In with Apple → Click Configure
- Set:
- Primary App ID: Select your app
- Domains:
YOUR_PROJECT_REF.supabase.co - Return URLs:
https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback
- Click Save → Continue → Save
Step 3.3: Create a Sign-In Key
- Go to Keys → Click + to add new
- Enter a Key Name (e.g., "Sign In with Apple Key")
- Check Sign In with Apple → Click Configure
- Select your Primary App ID
- Click Save → Continue → Register
- Download the key file (.p8) - you can only download it once!
- Note the Key ID
Step 3.4: Get Your Team ID
- Go to Apple Developer Account
- Scroll down to Membership details
- Copy your Team ID
Step 3.5: Configure Apple in Supabase
- Go to your Supabase Dashboard → Authentication → Providers
- Find Apple and click to expand
- Toggle Enable Sign in with Apple
- Enter:
- Service ID: The Service ID from Step 3.2 (e.g.,
com.yourcompany.yourapp.web) - Secret Key: Open the .p8 file in a text editor and paste the entire contents
- Key ID: From Step 3.3
- Team ID: From Step 3.4
- Service ID: The Service ID from Step 3.2 (e.g.,
- Click Save
Part 4: Flutter Implementation
Step 4.1: Add Dependencies
Update your pubspec.yaml:
dependencies: supabase_flutter: ^2.3.0 google_sign_in: ^6.2.1 sign_in_with_apple: ^6.1.0 crypto: ^3.0.3 # For generating nonce
Run:
flutter pub get
Step 4.2: iOS Configuration
Info.plist for Google Sign-In
Add to ios/Runner/Info.plist:
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <!-- Google Sign-In reversed client ID --> <string>com.googleusercontent.apps.YOUR_IOS_CLIENT_ID</string> </array> </dict> </array>
Replace YOUR_IOS_CLIENT_ID with your iOS Client ID from Google Cloud Console (reversed format).
Capabilities for Apple Sign-In
- Open
ios/Runner.xcworkspacein Xcode - Select your target → Signing & Capabilities
- Click + Capability → Add Sign In with Apple
Step 4.3: Android Configuration
No special configuration needed for Google Sign-In if you've added the SHA-1 fingerprint in Google Cloud Console.
For release builds, ensure you add the release keystore SHA-1 as well.
Step 4.4: Initialize Supabase
In your main.dart:
import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Supabase.initialize( url: 'https://YOUR_PROJECT_REF.supabase.co', anonKey: 'YOUR_ANON_KEY', ); runApp(const MyApp()); } // Global accessor for Supabase client final supabase = Supabase.instance.client;
Step 4.5: Create Authentication Service
Create lib/core/services/auth_service.dart:
import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:sign_in_with_apple/sign_in_with_apple.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class AuthService { final SupabaseClient _supabase; AuthService(this._supabase); /// Get current user User? get currentUser => _supabase.auth.currentUser; /// Check if user is logged in bool get isLoggedIn => currentUser != null; /// Stream of auth state changes Stream<AuthState> get authStateChanges => _supabase.auth.onAuthStateChange; /// Sign in with Google Future<AuthResponse> signInWithGoogle() async { // Web client ID from Google Cloud Console const webClientId = 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com'; // iOS client ID from Google Cloud Console const iosClientId = 'YOUR_IOS_CLIENT_ID.apps.googleusercontent.com'; final GoogleSignIn googleSignIn = GoogleSignIn( clientId: Platform.isIOS ? iosClientId : null, serverClientId: webClientId, ); final googleUser = await googleSignIn.signIn(); if (googleUser == null) { throw AuthException('Google sign in was cancelled'); } final googleAuth = await googleUser.authentication; final accessToken = googleAuth.accessToken; final idToken = googleAuth.idToken; if (accessToken == null) { throw AuthException('No access token from Google'); } if (idToken == null) { throw AuthException('No ID token from Google'); } return _supabase.auth.signInWithIdToken( provider: OAuthProvider.google, idToken: idToken, accessToken: accessToken, ); } /// Sign in with Apple (iOS only) Future<AuthResponse> signInWithApple() async { // Generate a random nonce final rawNonce = _generateRandomString(32); final hashedNonce = sha256.convert(utf8.encode(rawNonce)).toString(); final credential = await SignInWithApple.getAppleIDCredential( scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName, ], nonce: hashedNonce, ); final idToken = credential.identityToken; if (idToken == null) { throw AuthException('No ID token from Apple'); } return _supabase.auth.signInWithIdToken( provider: OAuthProvider.apple, idToken: idToken, nonce: rawNonce, ); } /// Sign out Future<void> signOut() async { // Sign out from Google if signed in try { final GoogleSignIn googleSignIn = GoogleSignIn(); await googleSignIn.signOut(); } catch (e) { debugPrint('Google sign out error: $e'); } // Sign out from Supabase await _supabase.auth.signOut(); } /// Generate random string for nonce String _generateRandomString(int length) { const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._'; final random = Random.secure(); return List.generate(length, (_) => charset[random.nextInt(charset.length)]) .join(); } } /// Custom auth exception class AuthException implements Exception { final String message; AuthException(this.message); String toString() => message; }
Step 4.6: Create Login Screen
Create lib/features/auth/presentation/screens/login_screen.dart:
import 'dart:io'; import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); State<LoginScreen> createState() => _LoginScreenState(); } class _LoginScreenState extends State<LoginScreen> { final _authService = AuthService(Supabase.instance.client); bool _isLoading = false; String? _errorMessage; Future<void> _signInWithGoogle() async { setState(() { _isLoading = true; _errorMessage = null; }); try { await _authService.signInWithGoogle(); // Navigation will be handled by auth state listener } catch (e) { setState(() { _errorMessage = e.toString(); }); } finally { if (mounted) { setState(() { _isLoading = false; }); } } } Future<void> _signInWithApple() async { setState(() { _isLoading = true; _errorMessage = null; }); try { await _authService.signInWithApple(); // Navigation will be handled by auth state listener } catch (e) { setState(() { _errorMessage = e.toString(); }); } finally { if (mounted) { setState(() { _isLoading = false; }); } } } Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Padding( padding: const EdgeInsets.all(24.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // App logo or title const Text( 'Create Account', style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Start your journey to better health', style: TextStyle( fontSize: 16, color: Colors.grey[600], ), textAlign: TextAlign.center, ), const SizedBox(height: 48), // Error message if (_errorMessage != null) Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(8), ), child: Text( _errorMessage!, style: TextStyle(color: Colors.red[900]), textAlign: TextAlign.center, ), ), // Google Sign In Button _SocialSignInButton( onPressed: _isLoading ? null : _signInWithGoogle, icon: 'G', label: 'Sign in with Google', isLoading: _isLoading, ), const SizedBox(height: 16), // Apple Sign In Button (iOS only) if (Platform.isIOS) ...[ _SocialSignInButton( onPressed: _isLoading ? null : _signInWithApple, icon: '', label: 'Sign in with Apple', isLoading: _isLoading, isApple: true, ), const SizedBox(height: 24), ], // Terms of service Text( 'By signing in, you agree to our Terms of Service and Privacy Policy', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), textAlign: TextAlign.center, ), ], ), ), ), ); } } class _SocialSignInButton extends StatelessWidget { final VoidCallback? onPressed; final String icon; final String label; final bool isLoading; final bool isApple; const _SocialSignInButton({ required this.onPressed, required this.icon, required this.label, this.isLoading = false, this.isApple = false, }); Widget build(BuildContext context) { return OutlinedButton( onPressed: onPressed, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), side: BorderSide(color: Colors.grey[300]!), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), ), child: isLoading ? const SizedBox( height: 24, width: 24, child: CircularProgressIndicator(strokeWidth: 2), ) : Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (isApple) const Icon(Icons.apple, size: 24) else Image.network( 'https://www.google.com/favicon.ico', height: 24, width: 24, errorBuilder: (_, __, ___) => const Text( 'G', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.blue, ), ), ), const SizedBox(width: 12), Text( label, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), ), ], ), ); } }
Step 4.7: Handle Auth State Changes
In your main app widget:
import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class MyApp extends StatelessWidget { const MyApp({super.key}); Widget build(BuildContext context) { return MaterialApp( title: 'My App', home: const AuthGate(), ); } } class AuthGate extends StatelessWidget { const AuthGate({super.key}); Widget build(BuildContext context) { return StreamBuilder<AuthState>( stream: Supabase.instance.client.auth.onAuthStateChange, builder: (context, snapshot) { // Check for loading state if (snapshot.connectionState == ConnectionState.waiting) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } // Check if user is logged in final session = snapshot.data?.session; if (session != null) { return const HomeScreen(); // Your main app screen } return const LoginScreen(); }, ); } }
Part 5: Advanced Configuration
Step 5.1: Store Additional User Data
When a user signs in, you might want to store additional profile data:
Future<void> _handleNewUser(User user) async { // Check if user already exists in your profiles table final existing = await Supabase.instance.client .from('profiles') .select() .eq('id', user.id) .maybeSingle(); if (existing == null) { // Create new profile await Supabase.instance.client.from('profiles').insert({ 'id': user.id, 'email': user.email, 'full_name': user.userMetadata?['full_name'] ?? user.userMetadata?['name'], 'avatar_url': user.userMetadata?['avatar_url'] ?? user.userMetadata?['picture'], 'created_at': DateTime.now().toIso8601String(), }); } }
Step 5.2: Create Profiles Table (SQL)
Run this in your Supabase SQL Editor:
-- Create profiles table create table public.profiles ( id uuid references auth.users on delete cascade primary key, email text, full_name text, avatar_url text, created_at timestamp with time zone default timezone('utc'::text, now()), updated_at timestamp with time zone default timezone('utc'::text, now()) ); -- Enable Row Level Security alter table public.profiles enable row level security; -- Create policies create policy "Users can view their own profile" on public.profiles for select using (auth.uid() = id); create policy "Users can update their own profile" on public.profiles for update using (auth.uid() = id); -- Create trigger for automatic profile creation create or replace function public.handle_new_user() returns trigger as $$ begin insert into public.profiles (id, email, full_name, avatar_url) values ( new.id, new.email, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url' ); return new; end; $$ language plpgsql security definer; create trigger on_auth_user_created after insert on auth.users for each row execute procedure public.handle_new_user();
Step 5.3: Deep Link Configuration (Optional)
For a smoother authentication flow, configure deep links:
iOS (Info.plist)
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLSchemes</key> <array> <string>YOUR_BUNDLE_ID</string> </array> </dict> </array>
Android (AndroidManifest.xml)
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="YOUR_PACKAGE_NAME" android:host="login-callback" /> </intent-filter>
Part 6: Testing
Step 6.1: Test Google Sign-In
- Run your app on a real device or emulator
- Tap "Sign in with Google"
- Complete the Google sign-in flow
- Verify user appears in Supabase Dashboard → Authentication → Users
Step 6.2: Test Apple Sign-In
- Run on a real iOS device (Apple Sign-In doesn't work on simulator)
- Tap "Sign in with Apple"
- Complete Face ID/Touch ID or password
- Verify user appears in Supabase Dashboard
Step 6.3: Verify in Supabase Dashboard
- Go to Authentication → Users
- You should see your test users with:
- Email address
- Provider (google, apple)
- Created at timestamp
Troubleshooting Common Issues
"Invalid client ID" (Google)
- Verify Web Client ID matches in both Google Cloud Console and Supabase
- Ensure redirect URI is correctly configured:
https://YOUR_PROJECT_REF.supabase.co/auth/v1/callback
"Sign in with Apple failed"
- Verify Service ID matches in both Apple Developer and Supabase
- Check that the domain and redirect URL are correctly configured
- Ensure the .p8 key content is complete (including BEGIN/END lines)
- Verify Team ID and Key ID are correct
"User not found" after sign-in
- Check that RLS policies allow the user to read their own data
- Verify the profiles table trigger is working
- Check Supabase logs for any errors
Google Sign-In not working on Android
- Verify SHA-1 fingerprint is added to Google Cloud Console
- For release builds, add release keystore SHA-1
- Ensure package name matches exactly
Security Best Practices
- Enable RLS - Always enable Row Level Security on tables
- Validate on Server - Don't trust client-side data
- Use HTTPS - Supabase handles this, but verify for any custom endpoints
- Secure API Keys - Never expose your service_role key
- Review Permissions - Regularly audit OAuth scopes and permissions
Checklist Before Production
- Google OAuth consent screen published (not in testing mode)
- All OAuth credentials created for production
- SHA-1 fingerprints added for release keystore
- Apple Sign-In configured with correct bundle ID
- Deep links configured for smooth auth flow
- RLS policies enabled and tested
- Error handling implemented for all auth flows
- Loading states shown during authentication
- Sign-out clears all auth state
Conclusion
Setting up Google and Apple Sign-In with Supabase provides a robust, secure authentication system for your Flutter app. The key benefits:
- User Experience - Familiar sign-in methods users trust
- Security - OAuth handles credentials, not your app
- Compliance - Apple Sign-In satisfies App Store requirements
- Scalability - Supabase handles millions of users
- Cost-Effective - Generous free tier for indie developers
The initial setup takes some time, but once configured, you have a production-ready authentication system that scales with your app.
Resources
- Supabase Auth Documentation
- Google Sign-In for Flutter
- Sign in with Apple for Flutter
- Apple Sign-In Guidelines
- Supabase Flutter SDK
Have questions? Found this helpful? Let me know on Twitter.


