Flutter Supabase Tutorial: Auth and Database Without Firebase

Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter Supabase Tutorial: Auth and Database Without Firebase, with supabase_flutter initialisation, signup and signinwithpassword, and select, insert, update, and delete.
Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter Supabase Tutorial: Auth and Database Without Firebase, with supabase_flutter initialisation, signup and signinwithpassword, and select, insert, update, and delete.

A realtime stream is more intuitive when a video shows the database update and Flutter rebuild together.

Subscribe on YouTube@codingliquids

Compare Postgres and Firebase trade-offs

Supabase exposes Postgres, SQL relationships, Auth, Storage, Realtime, and Row Level Security through one platform. It suits teams that want relational data and portable SQL, while Firebase offers a different document and ecosystem model.

Choosing solely by client syntax ignores data shape, operations, and team skills.

Initialise the Supabase client once

Call Supabase.initialize with the project URL and publishable or anon key before runApp. Read Supabase.instance.client after initialisation and inject it where repositories need access.

The client key is not a secret and cannot substitute for Row Level Security.

flutter pub add supabase_flutter

An Instagram RLS diagram puts client identity, table policy, and row ownership on the same request path.

Follow me on Instagram@sagnikteaches

Create sessions and observe auth changes

auth.signUp registers an email user, signInWithPassword creates a session, and onAuthStateChange emits lifecycle events. Use the event and session to gate authenticated UI and handle sign-out or token refresh.

Storing the password locally to recreate a session defeats the SDK session model.

The Complete Flutter Guide course thumbnail

Build production-ready Supabase Tutorial features

The Complete Flutter Guide turns supabase tutorial into maintainable app architecture, polished UI, and testable production code.

Enrol now

The supabase_flutter dependency supplies initialisation, Auth, PostgREST queries, and realtime streams.

dependencies:
  flutter:
    sdk: flutter
  supabase_flutter: any

Insert and select table rows

from(todos).insert sends column values and select returns rows allowed by current policies. Request only required columns, order explicitly, and map dynamic rows into a typed model.

An insert that succeeds without select permissions may not return the representation expected by the client.

import 'package:supabase_flutter/supabase_flutter.dart';

Update or delete a precise row

Chain update(values).eq(id, value) or delete().eq(id, value) so the mutation targets intended rows. Include ownership in Row Level Security rather than trusting a client-supplied user_id.

Calling update without a filter can alter every policy-visible row.

import 'package:supabase_flutter/supabase_flutter.dart';

Future<void> initialiseSupabase() => Supabase.initialize(
  url: 'https://YOUR_PROJECT.supabase.co',
  anonKey: 'YOUR_PUBLISHABLE_KEY',
);

final client = Supabase.instance.client;

Future<List<Map<String, dynamic>>> loadTodos() async =>
    await client.from('todos').select().order('created_at');

Stream<List<Map<String, dynamic>>> watchTodos() => client
    .from('todos')
    .stream(primaryKey: ['id'])
    .order('created_at');

The Supabase architecture discussion on LinkedIn covers migrations, realtime reconciliation, and why public keys are not authorisation.

Connect on LinkedInSagnik Bhattacharya
The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Stream realtime table changes

from(todos).stream with primaryKey combines initial rows and realtime updates into a Stream. Feed the stream to StreamBuilder and order it consistently for stable UI.

Realtime must be enabled for the table and policies still determine which rows arrive.

final auth = Supabase.instance.client.auth;
await auth.signUp(email: email, password: password);
await auth.signInWithPassword(email: email, password: password);

auth.onAuthStateChange.listen((event) {
  final signedIn = event.session != null;
  debugPrint('Auth event ${event.event}; signed in: $signedIn');
});

Make Row Level Security mandatory

Enable RLS and write select, insert, update, and delete policies using auth.uid ownership checks. Test policies with two different users and an unauthenticated client before release.

A hidden UI control or anon key restriction alone does not prevent direct REST calls.

await client.from('todos').insert({'title': 'Ship tutorial', 'done': false});
final rows = await client.from('todos').select('id,title,done').order('id');
await client.from('todos').update({'done': true}).eq('id', 42);
await client.from('todos').delete().eq('id', 42);

final todoStream = client.from('todos').stream(primaryKey: ['id']).order('id');

Put Row Level Security at the centre

The client’s project URL and public client key identify the Supabase project; they are not a substitute for authorisation. Enable Row Level Security on exposed tables and write policies that restrict each select, insert, update, and delete operation. A policy that permits selection does not automatically permit updates. Test as an anonymous visitor, as the row owner, and as a different authenticated user, including attempts to change the ownership column during an update.

Keep service-role credentials on a trusted server or function only. Shipping one in a Flutter asset, environment file, or obfuscated constant gives every installed copy administrative access. Client inserts should derive identity from the authenticated user recognised by the database policy rather than trusting a user ID typed into a payload. Constraints, foreign keys, and database functions can preserve invariants that would otherwise depend on every client version behaving perfectly.

Authentication redirects need exact platform configuration. Register the web origin and mobile deep-link scheme, handle the callback once, and wait for session recovery before deciding which route to show. Refresh-token persistence belongs behind the SDK and repository boundary; logs must never print a session object. Sign-out should dispose account-specific Realtime channels and clear local cached rows so another user cannot briefly see the previous session’s data.

Realtime subscriptions should be narrowed to the table and filter the screen needs, then removed when their owner is disposed. Events may arrive beside an optimistic local change, so reconcile by primary key and server version rather than blindly appending. Test initial query failure, expired sessions, policy denial, duplicate events, reconnect, and deletion while a detail route is open. Inspect PostgreSQL error codes and the Supabase response rather than converting every failure into “offline”. A production trace can name the operation, table, duration, and safe request ID while leaving row values and credentials out.

Schema migrations should be committed and repeatable rather than performed ad hoc in a production dashboard. Create tables, indexes, functions, grants, and policies through reviewed migrations, then apply the same history to local and staging projects. Generate or maintain client types after schema changes so a renamed nullable field does not become a runtime surprise. Integration tests can start with a known database, create two users, and prove both the allowed owner path and the denied cross-account path. A passing admin query is never evidence that the public client and Row Level Security policy work together.

Common mistakes

  • Compare Postgres and Firebase trade-offs: In Supabase auth and data, choosing solely by client syntax ignores data shape, operations, and team skills; inspect this Supabase auth and data cause before changing another Supabase auth and data widget.
  • Initialise the Supabase client once: In Supabase auth and data, the client key is not a secret and cannot substitute for Row Level Security; inspect this Supabase auth and data cause before changing another Supabase auth and data widget.
  • Create sessions and observe auth changes: In Supabase auth and data, storing the password locally to recreate a session defeats the SDK session model; inspect this Supabase auth and data cause before changing another Supabase auth and data widget.
  • Insert and select table rows: In Supabase auth and data, an insert that succeeds without select permissions may not return the representation expected by the client; inspect this Supabase auth and data cause before changing another Supabase auth and data widget.
  • Update or delete a precise row: In Supabase auth and data, calling update without a filter can alter every policy-visible row; inspect this Supabase auth and data cause before changing another Supabase auth and data widget.
  • Stream realtime table changes: In Supabase auth and data, realtime must be enabled for the table and policies still determine which rows arrive; inspect this Supabase auth and data cause before changing another Supabase auth and data widget.

Frequently asked questions

How does compare Postgres and Firebase trade-offs work in Supabase auth and data?

For Supabase auth and data, the starting rule is that supabase exposes Postgres, SQL relationships, Auth, Storage, Realtime, and Row Level Security through one platform. Apply this Supabase auth and data rule first because compare Postgres and Firebase trade-offs determines whether the Supabase auth and data pattern fits.

Why does create sessions and observe auth changes matter for Supabase auth and data?

In Supabase auth and data, use the event and session to gate authenticated UI and handle sign-out or token refresh. Keeping create sessions and observe auth changes at the Supabase auth and data call site exposes the Supabase auth and data return value directly.

What failure should I test first in Supabase auth and data?

First reproduce the Supabase auth and data case where calling update without a filter can alter every policy-visible row. The safe update identifies one intended row, satisfies Row Level Security, and leaves every neighbouring record unchanged.

How can I verify Supabase auth and data before release?

Exercise make Row Level Security mandatory with real Supabase auth and data inputs on every shipped platform. Inspect the final Supabase auth and data callback or output; a successful Supabase auth and data button tap alone is not proof.

Further reads

Connect Supabase auth and data to the surrounding Flutter stack through these published tutorials:

Sources: Supabase Flutter documentation; Supabase auth and data examples verified against current stable Flutter.