[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-backend-as-a-service-in-flutter-con-supabase-auth-database-e-realtime":3,"comments-article-backend-as-a-service-in-flutter-con-supabase-auth-database-e-realtime":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},80,"Backend as a Service in Flutter con Supabase: auth, database e realtime","backend-as-a-service-in-flutter-con-supabase-auth-database-e-realtime","Guida pratica a supabase_flutter: configurazione del progetto, autenticazione email e OAuth con deep link, query tipizzate su Postgres, stream realtime, Row Level Security e storage dei file. Con esempi di codice pronti all'uso e best practice architetturali.","Supabase è la principale alternativa open source a Firebase: un backend completo costruito su **PostgreSQL**, con autenticazione, API REST generate automaticamente, sottoscrizioni realtime, storage di file ed Edge Function. Per chi sviluppa in Flutter è una scelta interessante perché il pacchetto ufficiale `supabase_flutter` copre tutte le piattaforme (Android, iOS, Web, desktop) e perché avere un database relazionale vero significa poter usare join, viste e vincoli senza denormalizzare tutto come accade nei database a documenti.\n\nIn questo articolo vediamo come integrare Supabase in un'app Flutter reale: configurazione, autenticazione, accesso ai dati, realtime, sicurezza e struttura del codice.\n\n## Configurazione del progetto\n\nAggiungi la dipendenza:\n\n```yaml\ndependencies:\n  supabase_flutter: ^2.8.0\n```\n\nL'inizializzazione avviene una sola volta, prima di `runApp`:\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\nimport 'package:supabase_flutter\u002Fsupabase_flutter.dart';\n\nFuture\u003Cvoid> main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n\n  await Supabase.initialize(\n    url: const String.fromEnvironment('SUPABASE_URL'),\n    anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),\n    authOptions: const FlutterAuthClientOptions(\n      authFlowType: AuthFlowType.pkce, \u002F\u002F consigliato per i client mobile\n    ),\n  );\n\n  runApp(const MyApp());\n}\n\n\u002F\u002F\u002F Scorciatoia comoda per accedere al client\nfinal supabase = Supabase.instance.client;\n```\n\nLe chiavi vanno passate con `--dart-define` (o `--dart-define-from-file`), non hardcodate nel sorgente:\n\n```bash\nflutter run --dart-define=SUPABASE_URL=https:\u002F\u002Fxyz.supabase.co \\\n            --dart-define=SUPABASE_ANON_KEY=eyJhbGciOi...\n```\n\n> **Attenzione:** nell'app va usata **solo** la chiave `anon`. La chiave `service_role` bypassa tutte le policy di sicurezza e deve restare esclusivamente lato server (Edge Function o backend).\n\n## Autenticazione\n\n### Email e password\n\n```dart\nclass AuthRepository {\n  final SupabaseClient _client;\n  AuthRepository(this._client);\n\n  Future\u003Cvoid> signUp({required String email, required String password}) async {\n    await _client.auth.signUp(\n      email: email,\n      password: password,\n      data: {'display_name': email.split('@').first}, \u002F\u002F finisce in user_metadata\n    );\n  }\n\n  Future\u003Cvoid> signIn({required String email, required String password}) async {\n    await _client.auth.signInWithPassword(email: email, password: password);\n  }\n\n  Future\u003Cvoid> signOut() => _client.auth.signOut();\n\n  User? get currentUser => _client.auth.currentUser;\n\n  Stream\u003CAuthState> get onAuthStateChange => _client.auth.onAuthStateChange;\n}\n```\n\nIl client persiste automaticamente la sessione su disco e ne gestisce il refresh: al riavvio dell'app `currentUser` è già valorizzato, non serve implementare nulla a mano.\n\n### Reagire ai cambi di sessione\n\nIl modo più pulito per decidere cosa mostrare è ascoltare `onAuthStateChange`:\n\n```dart\nclass AuthGate extends StatelessWidget {\n  const AuthGate({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return StreamBuilder\u003CAuthState>(\n      stream: supabase.auth.onAuthStateChange,\n      builder: (context, snapshot) {\n        if (snapshot.connectionState == ConnectionState.waiting) {\n          return const Scaffold(\n            body: Center(child: CircularProgressIndicator()),\n          );\n        }\n        final session = snapshot.data?.session ?? supabase.auth.currentSession;\n        return session == null ? const LoginPage() : const HomePage();\n      },\n    );\n  }\n}\n```\n\nSe usi `go_router`, la stessa logica si traduce in un `redirect` con `refreshListenable` collegato allo stream.\n\n### Login OAuth e deep link\n\nPer Google, Apple o GitHub il flusso apre un browser esterno e torna nell'app tramite deep link:\n\n```dart\nFuture\u003Cvoid> signInWithGoogle() async {\n  await supabase.auth.signInWithOAuth(\n    OAuthProvider.google,\n    redirectTo: kIsWeb ? null : 'io.miaapp:\u002F\u002Flogin-callback\u002F',\n    authScreenLaunchMode: LaunchMode.externalApplication,\n  );\n}\n```\n\nSu Android va dichiarato l'intent filter in `AndroidManifest.xml`:\n\n```xml\n\u003Cintent-filter>\n  \u003Caction android:name=\"android.intent.action.VIEW\" \u002F>\n  \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n  \u003Ccategory android:name=\"android.intent.category.BROWSABLE\" \u002F>\n  \u003Cdata android:scheme=\"io.miaapp\" android:host=\"login-callback\" \u002F>\n\u003C\u002Fintent-filter>\n```\n\nSu iOS lo schema va aggiunto in `Info.plist` sotto `CFBundleURLTypes`. Lo stesso URL di redirect deve essere inserito nella whitelist del pannello Supabase (Authentication → URL Configuration).\n\nPer Apple e Google su mobile è preferibile il login nativo con `signInWithIdToken`, che evita il passaggio dal browser e rispetta le linee guida degli store.\n\n## Leggere e scrivere dati\n\nSupabase espone automaticamente un'API REST (PostgREST) su ogni tabella. Le query in Dart sono fluent e restituiscono `List\u003CMap\u003CString, dynamic>>`:\n\n```dart\nclass TodoRepository {\n  final SupabaseClient _client;\n  TodoRepository(this._client);\n\n  Future\u003CList\u003CTodo>> fetchTodos({int page = 0, int pageSize = 20}) async {\n    final data = await _client\n        .from('todos')\n        .select('id, title, is_done, created_at')\n        .eq('user_id', _client.auth.currentUser!.id)\n        .order('created_at', ascending: false)\n        .range(page * pageSize, (page + 1) * pageSize - 1);\n\n    return data.map(Todo.fromJson).toList();\n  }\n\n  Future\u003CTodo> addTodo(String title) async {\n    final row = await _client\n        .from('todos')\n        .insert({'title': title, 'user_id': _client.auth.currentUser!.id})\n        .select()\n        .single(); \u002F\u002F ritorna la riga appena creata\n    return Todo.fromJson(row);\n  }\n\n  Future\u003Cvoid> toggle(String id, bool isDone) async {\n    await _client.from('todos').update({'is_done': isDone}).eq('id', id);\n  }\n\n  Future\u003Cvoid> delete(String id) async {\n    await _client.from('todos').delete().eq('id', id);\n  }\n}\n```\n\nUn punto di forza rispetto ai database a documenti sono i **join impliciti** grazie alle foreign key:\n\n```dart\nfinal posts = await supabase\n    .from('posts')\n    .select('id, title, author:profiles(id, username, avatar_url), comments(count)')\n    .order('created_at', ascending: false)\n    .limit(20);\n```\n\nIn una sola richiesta ottieni il post, i dati dell'autore e il numero di commenti.\n\nPer logiche complesse conviene scrivere una funzione SQL e richiamarla con `rpc`:\n\n```dart\nfinal result = await supabase.rpc(\n  'search_posts',\n  params: {'query': 'flutter', 'max_results': 10},\n);\n```\n\n## Realtime: liste che si aggiornano da sole\n\nIl metodo `.stream()` apre una connessione WebSocket e restituisce uno `Stream` che emette l'intero set di righe a ogni modifica:\n\n```dart\nclass TodoListView extends StatefulWidget {\n  const TodoListView({super.key});\n\n  @override\n  State\u003CTodoListView> createState() => _TodoListViewState();\n}\n\nclass _TodoListViewState extends State\u003CTodoListView> {\n  late final Stream\u003CList\u003CTodo>> _stream;\n\n  @override\n  void initState() {\n    super.initState();\n    _stream = supabase\n        .from('todos')\n        .stream(primaryKey: ['id'])\n        .eq('user_id', supabase.auth.currentUser!.id)\n        .order('created_at')\n        .map((rows) => rows.map(Todo.fromJson).toList());\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return StreamBuilder\u003CList\u003CTodo>>(\n      stream: _stream,\n      builder: (context, snapshot) {\n        if (snapshot.hasError) return const Text('Errore di caricamento');\n        if (!snapshot.hasData) {\n          return const Center(child: CircularProgressIndicator());\n        }\n        final todos = snapshot.data!;\n        return ListView.builder(\n          itemCount: todos.length,\n          itemBuilder: (context, i) => CheckboxListTile(\n            title: Text(todos[i].title),\n            value: todos[i].isDone,\n            onChanged: (v) => supabase\n                .from('todos')\n                .update({'is_done': v})\n                .eq('id', todos[i].id),\n          ),\n        );\n      },\n    );\n  }\n}\n```\n\nDue avvertenze importanti:\n\n- il realtime va **abilitato sulla tabella** (`ALTER PUBLICATION supabase_realtime ADD TABLE todos;` o dal pannello);\n- `.stream()` supporta solo filtri semplici (`eq`, `neq`, `gt`, `inFilter`…) e non i join: per query complesse combina un `select()` iniziale con un canale realtime manuale.\n\nPer un controllo più fine puoi ascoltare i singoli eventi:\n\n```dart\nfinal channel = supabase\n    .channel('public:messages')\n    .onPostgresChanges(\n      event: PostgresChangeEvent.insert,\n      schema: 'public',\n      table: 'messages',\n      callback: (payload) {\n        final message = Message.fromJson(payload.newRecord);\n        _controller.add(message);\n      },\n    )\n    .subscribe();\n\n\u002F\u002F ricordati di chiudere il canale\n@override\nvoid dispose() {\n  supabase.removeChannel(channel);\n  super.dispose();\n}\n```\n\n## Sicurezza: la Row Level Security non è opzionale\n\nCon Supabase l'app parla direttamente col database: la sicurezza **non** sta nel codice Dart ma nelle policy PostgreSQL. Senza RLS attiva chiunque abbia la chiave `anon` (estraibile dal binario) può leggere l'intera tabella.\n\n```sql\nalter table todos enable row level security;\n\ncreate policy \"Gli utenti leggono i propri todo\"\n  on todos for select\n  using (auth.uid() = user_id);\n\ncreate policy \"Gli utenti creano i propri todo\"\n  on todos for insert\n  with check (auth.uid() = user_id);\n\ncreate policy \"Gli utenti modificano i propri todo\"\n  on todos for update\n  using (auth.uid() = user_id);\n```\n\nRegola pratica: attiva RLS su **ogni** tabella esposta, poi scrivi le policy. Se una query dal client restituisce una lista vuota quando ti aspetti dei dati, quasi sempre il colpevole è una policy mancante.\n\nPer default, inoltre, imposta le colonne come `user_id uuid references auth.users default auth.uid()`: eviti di doverle passare dal client.\n\n## Storage dei file\n\n```dart\nFuture\u003CString> uploadAvatar(File file) async {\n  final userId = supabase.auth.currentUser!.id;\n  final path = '$userId\u002Favatar.jpg';\n\n  await supabase.storage.from('avatars').upload(\n        path,\n        file,\n        fileOptions: const FileOptions(upsert: true, contentType: 'image\u002Fjpeg'),\n      );\n\n  \u002F\u002F bucket pubblico\n  return supabase.storage.from('avatars').getPublicUrl(path);\n\n  \u002F\u002F bucket privato: URL firmato valido 1 ora\n  \u002F\u002F return supabase.storage.from('avatars').createSignedUrl(path, 3600);\n}\n```\n\nAnche i bucket hanno le loro policy: strutturare i path con l'`uid` dell'utente come prima cartella rende banale scrivere regole del tipo `(storage.foldername(name))[1] = auth.uid()::text`.\n\n## Gestione degli errori\n\nIl SDK lancia eccezioni tipizzate: intercettale nel repository e traducile in errori di dominio.\n\n```dart\nFuture\u003CResult\u003CTodo>> addTodo(String title) async {\n  try {\n    final row = await _client.from('todos').insert({'title': title}).select().single();\n    return Success(Todo.fromJson(row));\n  } on PostgrestException catch (e) {\n    \u002F\u002F e.code '23505' = violazione di unique, '42501' = policy RLS\n    return Failure(DatabaseError(e.message, code: e.code));\n  } on AuthException catch (e) {\n    return Failure(AuthError(e.message));\n  } on SocketException {\n    return Failure(const NetworkError());\n  }\n}\n```\n\n## Best practice\n\n- **Isola il SDK dietro dei repository.** Non chiamare `supabase.from(...)` dentro i widget: rende impossibili i test e lega la UI al backend.\n- **Modelli tipizzati.** Le risposte sono mappe dinamiche: converti subito con `fromJson` generati da `freezed`\u002F`json_serializable`.\n- **PKCE e sessione.** Usa `AuthFlowType.pkce` sui client mobile e non salvare mai token a mano: ci pensa il SDK.\n- **Chiudi i canali realtime** in `dispose()`, altrimenti accumuli sottoscrizioni WebSocket.\n- **Paginazione con `range()`**, mai `select()` senza limiti su tabelle che possono crescere.\n- **Migrazioni versionate** con la Supabase CLI (`supabase migration new`), così lo schema vive nel repository insieme al codice Flutter.\n- **Offline first:** Supabase non ha una cache locale integrata come Firestore. Se ti serve, affianca un database locale (Drift o Isar) come single source of truth e sincronizza con `updated_at`.\n\n## Conclusioni\n\nSupabase offre a Flutter un backend completo in poche righe di configurazione, con il vantaggio di un database relazionale standard e la possibilità di self-hosting. Il modello mentale è però diverso da quello di un'API REST tradizionale: la logica di autorizzazione si sposta nelle policy SQL, e progettare bene schema e RLS fin dall'inizio è la parte più importante del lavoro. Fatto questo, il codice Dart resta sorprendentemente sottile: repository, modelli tipizzati e stream che aggiornano la UI in tempo reale.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fe691ce48-4359-4a07-984e-033cdccb4639.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1682559736721-c2e77ff4c650?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODc4ODk2ODl8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Lightsaber Collection","https:\u002F\u002Funsplash.com\u002F@lightsabercollection","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-bunch-of-wires-that-are-connected-to-a-server-T-IN5o3kxyA",null,"published","2026-08-28T04:01:29+00:00","Supabase e Flutter: auth, database e realtime","Guida pratica a Supabase in Flutter: setup di supabase_flutter, login email e OAuth, query su Postgres, stream realtime, Row Level Security e storage file.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789205510773]