Flutter App with Supabase
🟠Intermediate Estimated duration: 60 minutes
This tutorial shows you how to create a Flutter mobile application with a Supabase backend for authentication and database.
Objectives
By the end of this tutorial, you will know how to:
- Use
/dev:dev-supabaseto configure Supabase - Use
/dev:dev-flutterto create screens and widgets - Implement authentication with Supabase Auth
- Structure a Flutter app in Clean Architecture
Prerequisites
- Flutter SDK installed
- A Supabase account (free)
- An existing or new Flutter project
- Basic knowledge of Flutter/Dart
Context
We are going to create a notes app with:
- Email/password authentication
- Notes CRUD
- Real-time synchronization
- BLoC architecture
Step 1: Configure Supabase
Create the Supabase project
- Go to supabase.com
- Create a new project
- Note the URL and the anon key
Configure with claude-base
/dev:dev-supabase "Configure Supabase for a notes app with auth and CRUD"
Claude will create:
lib/core/supabase/supabase_client.dart
import 'package:supabase_flutter/supabase_flutter.dart';
class SupabaseService {
static SupabaseClient get client => Supabase.instance.client;
static Future<void> initialize() async {
await Supabase.initialize(
url: const String.fromEnvironment('SUPABASE_URL'),
anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),
);
}
static User? get currentUser => client.auth.currentUser;
static Stream<AuthState> get authStateChanges => client.auth.onAuthStateChange;
}
SQL Migration (to run in Supabase)
-- Notes table
create table notes (
id uuid default gen_random_uuid() primary key,
user_id uuid references auth.users(id) on delete cascade not null,
title text not null,
content text,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now()
);
-- RLS (Row Level Security)
alter table notes enable row level security;
create policy "Users can view their own notes"
on notes for select
using (auth.uid() = user_id);
create policy "Users can create their own notes"
on notes for insert
with check (auth.uid() = user_id);
create policy "Users can update their own notes"
on notes for update
using (auth.uid() = user_id);
create policy "Users can delete their own notes"
on notes for delete
using (auth.uid() = user_id);
Step 2: Create the model and repository
/dev:dev-flutter "Note entity and repository for CRUD operations with Supabase"
lib/features/notes/domain/entities/note.dart
import 'package:equatable/equatable.dart';
class Note extends Equatable {
final String id;
final String userId;
final String title;
final String? content;
final DateTime createdAt;
final DateTime updatedAt;
const Note({
required this.id,
required this.userId,
required this.title,
this.content,
required this.createdAt,
required this.updatedAt,
});
List<Object?> get props => [id, userId, title, content, createdAt, updatedAt];
}
lib/features/notes/data/repositories/note_repository_impl.dart
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../domain/entities/note.dart';
import '../../domain/repositories/note_repository.dart';
import '../models/note_model.dart';
class NoteRepositoryImpl implements NoteRepository {
final SupabaseClient _client;
NoteRepositoryImpl(this._client);
Future<List<Note>> getNotes() async {
final response = await _client
.from('notes')
.select()
.order('created_at', ascending: false);
return (response as List)
.map((json) => NoteModel.fromJson(json).toEntity())
.toList();
}
Future<Note> createNote(String title, String? content) async {
final userId = _client.auth.currentUser!.id;
final response = await _client.from('notes').insert({
'user_id': userId,
'title': title,
'content': content,
}).select().single();
return NoteModel.fromJson(response).toEntity();
}
Future<Note> updateNote(String id, String title, String? content) async {
final response = await _client.from('notes').update({
'title': title,
'content': content,
'updated_at': DateTime.now().toIso8601String(),
}).eq('id', id).select().single();
return NoteModel.fromJson(response).toEntity();
}
Future<void> deleteNote(String id) async {
await _client.from('notes').delete().eq('id', id);
}
Stream<List<Note>> watchNotes() {
return _client
.from('notes')
.stream(primaryKey: ['id'])
.order('created_at', ascending: false)
.map((data) => data.map((json) => NoteModel.fromJson(json).toEntity()).toList());
}
}
Step 3: Create the BLoC
/dev:dev-flutter "NotesBloc to manage notes state with CRUD events"
lib/features/notes/presentation/bloc/notes_bloc.dart
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import '../../domain/entities/note.dart';
import '../../domain/repositories/note_repository.dart';
// Events
abstract class NotesEvent extends Equatable {
List<Object?> get props => [];
}
class LoadNotes extends NotesEvent {}
class WatchNotes extends NotesEvent {}
class AddNote extends NotesEvent {
final String title;
final String? content;
AddNote(this.title, this.content);
List<Object?> get props => [title, content];
}
class UpdateNote extends NotesEvent {
final String id;
final String title;
final String? content;
UpdateNote(this.id, this.title, this.content);
List<Object?> get props => [id, title, content];
}
class DeleteNote extends NotesEvent {
final String id;
DeleteNote(this.id);
List<Object?> get props => [id];
}
// States
abstract class NotesState extends Equatable {
List<Object?> get props => [];
}
class NotesInitial extends NotesState {}
class NotesLoading extends NotesState {}
class NotesLoaded extends NotesState {
final List<Note> notes;
NotesLoaded(this.notes);
List<Object?> get props => [notes];
}
class NotesError extends NotesState {
final String message;
NotesError(this.message);
List<Object?> get props => [message];
}
// Bloc
class NotesBloc extends Bloc<NotesEvent, NotesState> {
final NoteRepository _repository;
StreamSubscription<List<Note>>? _subscription;
NotesBloc(this._repository): super(NotesInitial()) {
on<LoadNotes>(_onLoadNotes);
on<WatchNotes>(_onWatchNotes);
on<AddNote>(_onAddNote);
on<UpdateNote>(_onUpdateNote);
on<DeleteNote>(_onDeleteNote);
}
Future<void> _onLoadNotes(LoadNotes event, Emitter<NotesState> emit) async {
emit(NotesLoading());
try {
final notes = await _repository.getNotes();
emit(NotesLoaded(notes));
} catch (e) {
emit(NotesError(e.toString()));
}
}
Future<void> _onWatchNotes(WatchNotes event, Emitter<NotesState> emit) async {
_subscription?.cancel();
_subscription = _repository.watchNotes().listen(
(notes) => add(LoadNotes()),
onError: (e) => emit(NotesError(e.toString())),
);
}
Future<void> _onAddNote(AddNote event, Emitter<NotesState> emit) async {
try {
await _repository.createNote(event.title, event.content);
add(LoadNotes());
} catch (e) {
emit(NotesError(e.toString()));
}
}
Future<void> _onUpdateNote(UpdateNote event, Emitter<NotesState> emit) async {
try {
await _repository.updateNote(event.id, event.title, event.content);
add(LoadNotes());
} catch (e) {
emit(NotesError(e.toString()));
}
}
Future<void> _onDeleteNote(DeleteNote event, Emitter<NotesState> emit) async {
try {
await _repository.deleteNote(event.id);
add(LoadNotes());
} catch (e) {
emit(NotesError(e.toString()));
}
}
Future<void> close() {
_subscription?.cancel();
return super.close();
}
}
Step 4: Create the screens
/dev:dev-flutter "NotesListScreen with notes list, FAB to add and swipe to delete"
lib/features/notes/presentation/pages/notes_list_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../bloc/notes_bloc.dart';
import '../widgets/note_card.dart';
import 'note_editor_screen.dart';
class NotesListScreen extends StatelessWidget {
const NotesListScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Notes'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => _logout(context),
),
],
),
body: BlocBuilder<NotesBloc, NotesState>(
builder: (context, state) {
if (state is NotesLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is NotesError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(state.message),
ElevatedButton(
onPressed: () => context.read<NotesBloc>().add(LoadNotes()),
child: const Text('Retry'),
),
],
),
);
}
if (state is NotesLoaded) {
if (state.notes.isEmpty) {
return const Center(
child: Text('No notes. Create one!'),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: state.notes.length,
itemBuilder: (context, index) {
final note = state.notes[index];
return Dismissible(
key: Key(note.id),
direction: DismissDirection.endToStart,
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 16),
child: const Icon(Icons.delete, color: Colors.white),
),
onDismissed: (_) {
context.read<NotesBloc>().add(DeleteNote(note.id));
},
child: NoteCard(
note: note,
onTap: () => _editNote(context, note),
),
);
},
);
}
return const SizedBox.shrink();
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _createNote(context),
child: const Icon(Icons.add),
),
);
}
void _createNote(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const NoteEditorScreen()),
);
}
void _editNote(BuildContext context, Note note) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => NoteEditorScreen(note: note)),
);
}
void _logout(BuildContext context) async {
await SupabaseService.client.auth.signOut();
}
}
Step 5: Test with the emulator
flutter run
Verify that:
- Sign-up works
- Sign-in works
- Notes are created and displayed
- Updates are synchronized
Step 6: Mobile quality audit
/qa:qa-mobile
Claude will check:
- Rendering performance
- Memory management
- Accessibility
- Unit and widget tests
Step 7: Commit
/work:work-commit
Suggested message:
feat(notes): add notes feature with Supabase backend
- Add Supabase configuration with RLS
- Add Note entity and repository
- Add NotesBloc for state management
- Add NotesListScreen with swipe to delete
- Add NoteEditorScreen for create/update
- Add real-time synchronization
Final structure
lib/
├── core/
│ └── supabase/
│ └── supabase_client.dart
├── features/
│ └── notes/
│ ├── data/
│ │ ├── models/
│ │ │ └── note_model.dart
│ │ └── repositories/
│ │ └── note_repository_impl.dart
│ ├── domain/
│ │ ├── entities/
│ │ │ └── note.dart
│ │ └── repositories/
│ │ └── note_repository.dart
│ └── presentation/
│ ├── bloc/
│ │ └── notes_bloc.dart
│ ├── pages/
│ │ ├── notes_list_screen.dart
│ │ └── note_editor_screen.dart
│ └── widgets/
│ └── note_card.dart
└── config/
└── injection.dart
Next steps
Supabase RLS
Always enable Row Level Security on your Supabase tables. Claude configures it automatically with /dev:dev-supabase.