Agent SkillsAgent Skills
abbas133

flutter-supabase

@abbas133/flutter-supabase
abbas133
0
0 forks
Updated 4/13/2026
View on GitHub

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

$npx agent-skills-cli install @abbas133/flutter-supabase
Claude Code
Cursor
Copilot
Codex
Antigravity

Details

Patheisaal-app/.claude/skills/flutter-supabase/SKILL.md
Branchmain
Scoped Name@abbas133/flutter-supabase

Usage

After installing, this skill will be available to your AI coding assistant.

Verify installation:

npx agent-skills-cli list

Skill 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');