Supabase uses PostgreSQL under the hood. You interact with it from Flutter using the Supabase client with a simple, chainable API. This guide covers all the CRUD operations, filtering, sorting, and row-level security.
Assumes Supabase is initialized. See Auth for setup.
Select (Read)
Fetch data from a table. You can select all columns, specific columns, or even pull in related data from other tables in a single query.
// Get all rows from a table
final data = await supabase.from('posts').select();
// Only specific columns
final data = await supabase.from('posts').select('id, title, created_at');
// Join related data (posts with their author's profile)
final data = await supabase
.from('posts')
.select('id, title, author:profiles(name, avatar_url)');
// Get a single row by ID
final data = await supabase
.from('posts')
.select()
.eq('id', postId)
.single();Insert (Create)
Add new rows to a table. Pass a map of column names to values. Chain .select().single() if you want the newly created row returned (useful for getting the auto-generated ID).
// Insert a row
await supabase.from('posts').insert({
'title': 'My First Post',
'content': 'Hello world',
'user_id': supabase.auth.currentUser!.id,
});
// Insert and get the new row back (with generated ID, timestamps, etc.)
final newPost = await supabase.from('posts').insert({
'title': 'My First Post',
'content': 'Hello world',
'user_id': supabase.auth.currentUser!.id,
}).select().single();
// Insert multiple rows at once
await supabase.from('posts').insert([
{'title': 'Post 1', 'content': 'First'},
{'title': 'Post 2', 'content': 'Second'},
]);Update
Modify existing rows. Always chain a filter (like .eq()) to target specific rows. Without a filter, you would update every row in the table.
// Update a specific row
await supabase
.from('posts')
.update({'title': 'Updated Title'})
.eq('id', postId);
// Update and get the result back
final updated = await supabase
.from('posts')
.update({'title': 'Updated Title'})
.eq('id', postId)
.select()
.single();Delete
Remove rows from a table. Like update, always use a filter to target specific rows.
await supabase
.from('posts')
.delete()
.eq('id', postId);Filters
Chain filters after .select(), .update(), or .delete() to target specific rows. Multiple filters combine with AND logic (all conditions must be true).
// Equals / Not equals
.eq('status', 'published')
.neq('status', 'draft')
// Comparison
.gt('views', 100) // greater than
.gte('rating', 4) // greater than or equal
.lt('price', 50) // less than
.lte('age', 30) // less than or equal
// Pattern matching (% is a wildcard)
.like('title', '%flutter%') // contains "flutter"
.ilike('title', '%flutter%') // case-insensitive
// Value in a list
.inFilter('status', ['published', 'featured'])
// Check for null
.isFilter('deleted_at', null)
// Array column contains a value
.contains('tags', ['flutter'])Ordering & Pagination
Sort results and limit how many rows you get back. Use .range() for pagination (0-indexed, inclusive on both ends).
// Newest first
final data = await supabase
.from('posts')
.select()
.order('created_at', ascending: false);
// Only get the first 10
final data = await supabase
.from('posts')
.select()
.order('created_at', ascending: false)
.limit(10);
// Page 2 (rows 10-19)
final data = await supabase
.from('posts')
.select()
.order('created_at', ascending: false)
.range(10, 19);Row Level Security (RLS)
RLS controls who can access which rows. You define policies in the Supabase dashboard (Table Editor → Policies, or SQL Editor). Policies use auth.uid()which returns the currently logged-in user's ID, so you can restrict users to only their own data.
Make sure your tables have a user_id column that stores the owner. Then create policies for each operation:
-- Users can only read their own posts
CREATE POLICY "Users read own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Users can only create posts as themselves
CREATE POLICY "Users insert own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can only update their own posts
CREATE POLICY "Users update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- Users can only delete their own posts
CREATE POLICY "Users delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);With RLS enabled, if a user tries to read or modify someone else's data, the query returns empty results (not an error). This happens automatically at the database level, so your Flutter code does not need to add extra filters.
Count
Get the total number of matching rows without fetching the data itself. Useful for pagination (showing "Page 1 of 12").
final count = await supabase
.from('posts')
.count()
.eq('status', 'published');
// count is an intRPC (Database Functions)
For complex queries that are hard to express with the client API, write a PostgreSQL function and call it from Flutter. Create it in the Supabase SQL Editor:
CREATE OR REPLACE FUNCTION get_popular_posts(min_views int)
RETURNS SETOF posts AS $$
SELECT * FROM posts
WHERE views >= min_views
ORDER BY views DESC;
$$ LANGUAGE sql;Call it from Flutter by name, passing parameters as a map:
final data = await supabase.rpc('get_popular_posts', params: {
'min_views': 100,
});Error Handling
Supabase throws a PostgrestException when a query fails. Wrap database calls in try/catch to handle errors like constraint violations, permission denied (RLS), or network issues.
try {
await supabase.from('posts').insert({
'title': 'New Post',
'user_id': supabase.auth.currentUser!.id,
});
} on PostgrestException catch (e) {
print('Database error: ${e.message}');
print('Code: ${e.code}');
// Common codes:
// '23505' — unique constraint violation (duplicate)
// '42501' — RLS permission denied
// '23503' — foreign key violation
} catch (e) {
print('Unexpected error: $e');
}