Expert Supabase integration for Flutter. Use when working with Supabase auth, database queries, realtime subscriptions, storage, edge functions, or RLS policies. Covers supabase_flutter package patterns.
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
name: flutter-supabase description: Expert Supabase integration for Flutter. Use when working with Supabase auth, database queries, realtime subscriptions, storage, edge functions, or RLS policies. Covers supabase_flutter package patterns.
Flutter Supabase Skill
Setup
Dependencies
dependencies:
supabase_flutter: ^2.5.0
Initialize (main.dart)
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'YOUR_SUPABASE_URL',
anonKey: 'YOUR_ANON_KEY',
);
runApp(MyApp());
}
// Global client access
final supabase = Supabase.instance.client;
With Riverpod
@Riverpod(keepAlive: true)
SupabaseClient supabaseClient(Ref ref) {
return Supabase.instance.client;
}
Authentication
Email/Password Sign Up
Future<AuthResponse> signUp(String email, String password) async {
return await supabase.auth.signUp(
email: email,
password: password,
);
}
Email/Password Sign In
Future<AuthResponse> signIn(String email, String password) async {
return await supabase.auth.signInWithPassword(
email: email,
password: password,
);
}
Google OAuth
Future<void> signInWithGoogle() async {
await supabase.auth.signInWithOAuth(
OAuthProvider.google,
redirectTo: 'io.supabase.yourapp://login-callback/',
);
}
Sign Out
Future<void> signOut() async {
await supabase.auth.signOut();
}
Get Current User
User? get currentUser => supabase.auth.currentUser;
// Or with null check
User getCurrentUser() {
final user = supabase.auth.currentUser;
if (user == null) throw Exception('Not authenticated');
return user;
}
Auth State Stream
@riverpod
Stream<AuthState> authState(Ref ref) {
return supabase.auth.onAuthStateChange;
}
// Usage in widget
ref.listen(authStateProvider, (prev, next) {
next.whenData((state) {
if (state.event == AuthChangeEvent.signedOut) {
context.go('/login');
}
});
});
Database Queries
Select All
Future<List<Map<String, dynamic>>> getAll() async {
final response = await supabase
.from('prayers')
.select();
return response;
}
Select with Columns
Future<List<Map<String, dynamic>>> getNames() async {
final response = await supabase
.from('prayers')
.select('id, name, time');
return response;
}
Select with Filter
Future<List<Map<String, dynamic>>> getByCategory(String category) async {
final response = await supabase
.from('duas')
.select()
.eq('category', category)
.order('name');
return response;
}
Select Single Row
Future<Map<String, dynamic>> getById(String id) async {
final response = await supabase
.from('users')
.select()
.eq('id', id)
.single();
return response;
}
Select with Relations (Joins)
Future<List<Map<String, dynamic>>> getPrayersWithMosque() async {
final response = await supabase
.from('prayers')
.select('''
id,
name,
time,
mosque:mosques(id, name, address)
''');
return response;
}
Insert
Future<Map<String, dynamic>> create(Map<String, dynamic> data) async {
final response = await supabase
.from('prayers')
.insert(data)
.select()
.single();
return response;
}
Insert Multiple
Future<List<Map<String, dynamic>>> createMany(List<Map<String, dynamic>> data) async {
final response = await supabase
.from('prayers')
.insert(data)
.select();
return response;
}
Update
Future<Map<String, dynamic>> update(String id, Map<String, dynamic> data) async {
final response = await supabase
.from('prayers')
.update(data)
.eq('id', id)
.select()
.single();
return response;
}
Upsert
Future<Map<String, dynamic>> upsert(Map<String, dynamic> data) async {
final response = await supabase
.from('prayers')
.upsert(data)
.select()
.single();
return response;
}
Delete
Future<void> delete(String id) async {
await supabase
.from('prayers')
.delete()
.eq('id', id);
}
Filter Operators
// Equal
.eq('column', value)
// Not equal
.neq('column', value)
// Greater than
.gt('column', value)
// Greater than or equal
.gte('column', value)
// Less than
.lt('column', value)
// Less than or equal
.lte('column', value)
// Pattern match (LIKE)
.like('name', '%prayer%')
// Case insensitive pattern
.ilike('name', '%Prayer%')
// In array
.inFilter('id', ['1', '2', '3'])
// Contains (for arrays)
.contains('tags', ['ramadan'])
// Is null
.isFilter('deleted_at', null)
// Range
.range(0, 9) // First 10 rows
// Full text search
.textSearch('name', 'fajr')
Ordering & Pagination
final response = await supabase
.from('duas')
.select()
.order('created_at', ascending: false)
.range(0, 19); // First 20 items
Realtime Subscriptions
Subscribe to Table Changes
@riverpod
Stream<List<Map<String, dynamic>>> prayerTimesStream(Ref ref) {
return supabase
.from('prayer_times')
.stream(primaryKey: ['id'])
.order('time');
}
Subscribe with Filter
Stream<List<Map<String, dynamic>>> userPrayersStream(String userId) {
return supabase
.from('prayers')
.stream(primaryKey: ['id'])
.eq('user_id', userId);
}
Broadcast Channels
final channel = supabase.channel('room1');
channel
.onBroadcast(event: 'cursor', callback: (payload) {
print('Received: $payload');
})
.subscribe();
// Send
channel.sendBroadcastMessage(
event: 'cursor',
payload: {'x': 100, 'y': 200},
);
Storage
Upload File
Future<String> uploadImage(String path, Uint8List bytes) async {
await supabase.storage
.from('avatars')
.uploadBinary(path, bytes);
return supabase.storage
.from('avatars')
.getPublicUrl(path);
}
Upload from File
Future<String> uploadFile(String path, File file) async {
await supabase.storage
.from('documents')
.upload(path, file);
return supabase.storage
.from('documents')
.getPublicUrl(path);
}
Download File
Future<Uint8List> downloadFile(String path) async {
return await supabase.storage
.from('documents')
.download(path);
}
Get Signed URL (Time-Limited)
Future<String> getSignedUrl(String path) async {
return await supabase.storage
.from('private')
.createSignedUrl(path, 3600); // 1 hour
}
Delete File
Future<void> deleteFile(String path) async {
await supabase.storage
.from('avatars')
.remove([path]);
}
List Files
Future<List<FileObject>> listFiles(String folder) async {
return await supabase.storage
.from('documents')
.list(path: folder);
}
Edge Functions
Future<Map<String, dynamic>> callFunction(
String name,
Map<String, dynamic> body,
) async {
final response = await supabase.functions.invoke(
name,
body: body,
);
return response.data;
}
// Example
final result = await callFunction('calculate-khums', {
'income': 50000,
'expenses': 30000,
'marja': 'sistani',
});
Error Handling Pattern
Future<Either<Failure, User>> getUser(String id) async {
try {
final response = await supabase
.from('users')
.select()
.eq('id', id)
.single();
return Right(UserModel.fromJson(response).toEntity());
} on PostgrestException catch (e) {
return Left(DatabaseFailure(message: e.message));
} on AuthException catch (e) {
return Left(AuthFailure(message: e.message));
} catch (e) {
return Left(ServerFailure(message: e.toString()));
}
}
Repository Pattern
// lib/modules/prayers/data/datasources/prayer_remote_datasource.dart
class PrayerRemoteDataSource {
final SupabaseClient _client;
PrayerRemoteDataSource(this._client);
Future<List<PrayerModel>> getAll() async {
final response = await _client
.from('prayers')
.select()
.order('time');
return response.map((e) => PrayerModel.fromJson(e)).toList();
}
Future<PrayerModel> getById(String id) async {
final response = await _client
.from('prayers')
.select()
.eq('id', id)
.single();
return PrayerModel.fromJson(response);
}
Stream<List<PrayerModel>> watchAll() {
return _client
.from('prayers')
.stream(primaryKey: ['id'])
.order('time')
.map((data) => data.map((e) => PrayerModel.fromJson(e)).toList());
}
}
RLS (Row Level Security) Notes
Always design queries assuming RLS is enabled:
-- Example RLS policy
CREATE POLICY "Users can view own data"
ON prayers FOR SELECT
USING (auth.uid() = user_id);
-- Your Dart code doesn't need .eq('user_id', userId)
-- RLS automatically filters!
Common Mistakes
// β Not awaiting
supabase.from('prayers').select(); // Missing await!
// β
Correct
await supabase.from('prayers').select();
// β Using .single() when multiple rows possible
final response = await supabase.from('prayers').select().single();
// β
Use .maybeSingle() for optional single row
final response = await supabase.from('prayers').select().maybeSingle();
// β Forgetting to handle null
final user = supabase.auth.currentUser;
user.id; // Might be null!
// β
Null check
final user = supabase.auth.currentUser;
if (user == null) throw Exception('Not authenticated');
More by abbas133
View allExpert UI/UX design and frontend development guidance. Use when designing interfaces, creating design systems, improving user experience, implementing accessible components, or building responsive layouts. Covers WCAG compliance, design patterns, and CSS/component best practices.
Expert Flutter/Dart guidance. Use when working with Flutter projects, implementing BLoC/Riverpod patterns, writing Dart code, optimizing performance, or following Clean Architecture. Provides architecture patterns, widget best practices, and testing strategies.
Expert Firebase integration for Flutter. Use when working with Firebase Auth, Firestore, Cloud Storage, Cloud Messaging (FCM), Analytics, Crashlytics, or Remote Config. Covers FlutterFire packages and best practices.
Complete Eisaal Sanctuary design system including Karbala shrine-inspired color palette, typography, spacing tokens, glassmorphic effects, and animation patterns. Use for any UI work.
