Mobilni razvoj
FlutterSupabaseAutentifikacijaRealtimeOffline-firstRLSRazvoj mobilnih aplikacijaPostgreSQL

Flutter + Supabase u produkciji: Auth, Realtime, RLS i offline-pristup podacima (Vodič za 2026.)

AO
Adrijan Omićević
·15 min čitanja

# Što ćete izgraditi (i zašto je to važno)#

Ovaj vodič pokazuje kako isporučiti Flutter aplikacije sa Supabaseom u produkciji, s fokusom na četiri područja koja se najčešće “razbiju” nakon lansiranja: autentifikacijske tokove, siguran pristup podacima uz Row Level Security, realtime ažuriranja i offline-prijateljski pristup podacima.

Ako odlučujete između Firebasea i Supabasea, prvo pročitajte naš Flutter Firebase tutorial kako biste razumjeli što Firebase daje “iz kutije”, posebno offline sync, a zatim iskoristite ovaj vodič da implementirate sličan UX na Supabaseu.

Također ćete vidjeti kako strukturirati kod tako da se auth, podaci i sinkronizacija ne “prelijevaju” kroz cijeli codebase. Za opcije arhitekture pogledajte Flutter app architecture: clean architecture, feature-first.

# Preduvjeti#

ZahtjevVerzijaNapomene
Flutter3.19+Preporučeno stable
Dart3.3+Dolazi uz Flutter
Supabase projektBilo kojiUključite Email auth i Realtime
supabase_flutterNajnovijiSlužbeni klijent za Flutter
Local storagedrift ili isarZa offline cache i outbox
Secure storageflutter_secure_storageZa tokene i osjetljive zastavice

ℹ️ Napomena: Supabase Auth sesije supabase_flutter sprema (persistira) automatski. Ipak, trebate razumjeti gdje i kako se tokeni pohranjuju te kako riješiti refresh sesije, logout i edge-caseove s više uređaja.

# 1) Model podataka koji preživi produkciju#

Prije nego napišete Flutter kod, zaključajte shemu koja podržava RLS i offline sync. Tipičan primjer “tasks” je dovoljan da dokaže obrasce.

Preporučena shema#

Ove stupce želite gotovo uvijek:

  • id UUID primarni ključ
  • user_id UUID vlasnik
  • created_at, updated_at
  • deleted_at nullable timestamp za soft delete
  • version integer za detekciju konflikata
  • client_updated_at timestamp koji postavlja klijent, koristi se za usklađivanje

U PostgreSQL terminima:

StupacTipZašto
iduuidStabilan ID za zapise kreirane offline
user_iduuidRLS provjere vlasništva
updated_attimestamptzServer-side izvor istine za sortiranje
client_updated_attimestamptzPomaže kad se offline izmjene događaju na više uređaja
versionintDetektira izgubljena ažuriranja, podržava strategije spajanja (merge)
deleted_attimestamptz nullable“Tombstones” za sync i realtime

💡 Savjet: Preferirajte UUID-ove generirane na klijentu za offline kreiranja. Možete ih generirati u Flutteru i ubaciti kasnije, tako da UI može navigirati na detalje bez čekanja mreže.

# 2) Supabase Auth u Flutteru: flowovi koji vas kasnije ne ugrizu#

Produkcijski auth se većinom svodi na edge-caseove: refresh sesije, deep linkove, više providera i “polu ulogirana” stanja.

2.1 Instalacija i inicijalizacija Supabasea#

Dart
// main.dart
import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
 
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
 
  await Supabase.initialize(
    url: const String.fromEnvironment('SUPABASE_URL'),
    anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY'),
  );
 
  runApp(const MyApp());
}
 
final supabase = Supabase.instance.client;

Izbjegavajte hardkodiranje ključeva. Koristite --dart-define po okruženju.

Bash
flutter run \
  --dart-define=SUPABASE_URL=your-url \
  --dart-define=SUPABASE_ANON_KEY=your-anon-key

2.2 Email i lozinka (s produkcijskim UX-om)#

U produkciji je minimum:

  • client-side validacija formata emaila i duljine lozinke
  • jasne poruke grešaka za rate limit i nevažeće kredencijale
  • potvrda emaila ako je required
  • reset lozinke
Dart
Future<void> signInWithEmail(String email, String password) async {
  final res = await supabase.auth.signInWithPassword(
    email: email,
    password: password,
  );
  if (res.session == null) {
    throw Exception('No session returned');
  }
}

Za sign up:

Dart
Future<void> signUp(String email, String password) async {
  await supabase.auth.signUp(
    email: email,
    password: password,
    data: {'marketing_opt_in': false},
  );
}

2.3 OAuth provideri i deep linkovi#

Ako podržavate Google ili Apple, testirajte deep linkove na:

  • Androidu: više preglednika, custom tabs
  • iOS-u: Safari i in-app browser
  • cold start i warm start

Provjerite da su redirect URL-ovi ispravno konfigurirani u Supabaseu i u vašoj aplikaciji.

⚠️ Upozorenje: OAuth koji “radi na mom uređaju” često padne u produkciji zbog pogrešnih redirect URL-ova, nedostajućeg SHA-256 fingerprinta za Android ili Universal Linkova koji ne odgovaraju produkcijskom bundle ID-u. Testirajte release build rano.

2.4 Životni ciklus sesije: slušajte jednom, rutajte ispravno#

Trebate jedan izvor istine za auth stanje.

Dart
Stream<AuthState> authStateChanges() {
  return supabase.auth.onAuthStateChange.map((e) => e);
}

Logika rutiranja treba razlikovati:

  • signed out
  • signed in
  • token refreshing
  • password recovery ili email confirmation flowove

Za produkciju implementirajte “soft logout” na auth greške: obrišite lokalni cache tek kada ste sigurni da je korisnik odjavljen, a ne kad se dogodi prolazna mrežna greška.

2.5 Preporučeni obrazac: AuthRepository + session guard#

Povežite ovo s clean architecture kako UI ne bi zvao Supabase direktno. Za feature-first pristup slijedite naš vodič za Flutter app architecture.

SlojOdgovornostTreba znati za Supabase client
UIForme, rutiranje, view stateNe
AuthRepositorySign-in/out, session streamDa
Use caseoviOrkestracija “SignIn”, “SignOut”Ne
Data sourceoviSupabase API poziviDa

# 3) Siguran pristup podacima s RLS-om: dio bez pregovora#

RLS je razlog zašto je Supabase produkcijski ozbiljan za mobile. Bez njega, bilo tko može queryati vašu bazu kopiranjem anon ključa.

3.1 Uključite RLS i zabranite sve po defaultu#

Za svaku tablicu izloženu klijentu:

  1. 1
    uključite RLS
  2. 2
    napravite eksplicitne policyje za select, insert, update, delete

U SQL-u:

SQL
alter table public.tasks enable row level security;
 
-- Read only your own tasks
create policy "tasks_select_own"
on public.tasks for select
using (auth.uid() = user_id);
 
-- Insert only with your user_id
create policy "tasks_insert_own"
on public.tasks for insert
with check (auth.uid() = user_id);
 
-- Update only your own
create policy "tasks_update_own"
on public.tasks for update
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
 
-- Soft delete only your own
create policy "tasks_delete_own"
on public.tasks for delete
using (auth.uid() = user_id);

3.2 Izbjegnite vjerovati user_id koji šalje klijent#

Čak i uz RLS, želite smanjiti “footgunove”. Koristite database default za user_id gdje god možete.

SQL
alter table public.tasks
alter column user_id set default auth.uid();

Sada klijent ne mora slati user_id na insert, i uklanjate cijelu klasu grešaka.

🎯 Ključna poruka: Vaša Flutter aplikacija je nepouzdan klijent. Enforceajte vlasništvo i dozvole u PostgreSQL-u, ne u Dart kodu.

3.3 Multi-tenant podaci: timovi i članstva#

Ako vaša aplikacija ima timove, trebate tablice članstva i policyje temeljene na joinovima.

Čest raspored:

TablicaKljučni stupciSvrha
teamsid, created_byEntitet tima
team_membersteam_id, user_id, roleČlanstvo i uloga
tasksid, team_id, created_bySadržaj u okviru tima

Ideja policyja: dopusti pristup ako je korisnik član tima.

SQL
create policy "tasks_select_team_member"
on public.tasks for select
using (
  exists (
    select 1
    from public.team_members m
    where m.team_id = tasks.team_id
      and m.user_id = auth.uid()
  )
);

3.4 Česte RLS zamke#

  1. 1

    Zaboravljanje with check na insert i update
    using kontrolira koji su redci vidljivi ili “ciljivi”, dok with check kontrolira koji su novi podaci dopušteni.

  2. 2

    Pogrešno korištenje security definer funkcija
    Ako radite RPC funkcije, razumite zaobilaze li RLS. Držite ih minimalnima i auditanima.

  3. 3

    Neusklađenost Realtime-a i RLS-a
    Realtime eventi mogu izgledati kao da “nedostaju” ako korisnik prema RLS-u nema pravo vidjeti redak. To je ispravno ponašanje, ali tijekom testiranja izgleda kao bug.

# 4) Realtime u Flutteru: pretplate koje skaliraju#

Supabase Realtime je odličan za kolaborativna ažuriranja, live dashboarde i chat-slična iskustva. Također može uništiti performanse ako se pretplatite preširoko.

4.1 Pretplatite se usko i samo kad treba#

Primjer: slušajte taskove za jedan tim.

Dart
RealtimeChannel subscribeToTasks(String teamId, void Function(Map<String, dynamic>) onChange) {
  final channel = supabase.channel('tasks:$teamId');
 
  channel.onPostgresChanges(
    event: PostgresChangeEvent.all,
    schema: 'public',
    table: 'tasks',
    filter: PostgresChangeFilter(
      type: PostgresChangeFilterType.eq,
      column: 'team_id',
      value: teamId,
    ),
    callback: (payload) => onChange(payload.newRecord),
  );
 
  channel.subscribe();
  return channel;
}

Pravilo životnog ciklusa: subscribe na ulasku u ekran, unsubscribe u disposeu.

4.2 Realtime + lokalni cache: tretirajte realtime kao invalidaciju#

Ako ste offline-first, realtime ne smije biti primarni izvor podataka. Koristite ga da invalidira ili patcha lokalni cache.

Praktično pravilo:

  • ako redak imate lokalno, mergeajte payload
  • ako ga nemate, zakažite background fetch za tu stranicu ili query

4.3 Throttlajte UI ažuriranja#

Ako se pretplatite na promjene visoke frekvencije, ne rebuildajte cijeli popis za svaki event. Batchajte ažuriranja 100 do 300 ms, zatim primijenite jedno stanje (state update).

⚠️ Upozorenje: Pretplata na “sve retke u tablici” najbrži je način za pražnjenje baterije, previše rebuildova i teško debugabilne data leakove ako su filteri pogrešni.

# 5) Offline-prijateljski pristup podacima: što Supabase ne radi umjesto vas#

Firebaseova “killer” značajka za mnoge aplikacije je ugrađena offline perzistencija i rješavanje konflikata. Supabase je PostgreSQL-first, pa offline UX morate implementirati namjerno.

Za strategije konflikata i primjere iz prakse pročitajte Flutter offline-first sync conflict resolution.

5.1 Ciljani UX: “trenutni UI” uz eventualnu konzistentnost#

Realističan offline-first cilj izgleda ovako:

  • čitanja se poslužuju iz lokalne baze odmah
  • upisi odmah ažuriraju lokalnu bazu i stavljaju outbox event u red
  • sync worker pokušava ponovo u pozadini
  • konflikti se detektiraju i rješavaju predvidljivo

5.2 Preporučeni pristup lokalnoj pohrani#

Za produkcijske Flutter aplikacije dvije su česte opcije:

Lokalna DBPrednostKada odabrati
drift (SQLite)Snažni upiti, migracije, relacijsko modeliranjeBusiness aplikacije s kompleksnim filterima
isarBrz object store, jednostavna perzistencijaJednostavnije sheme, performanse na prvom mjestu

Ako se već oslanjate na SQL na serveru, drift često “sjedne” prirodno jer možete preslikati query obrasce lokalno.

5.3 Outbox tablica: queueanje upisa dok ste offline#

Vaša lokalna baza treba uključivati tablicu outbox sa:

PoljePrimjerSvrha
idUUIDJedinstveni ID eventa
entity"tasks"Koja tablica
entity_idUUIDKoji redak
op"insert" or "update" or "delete"Tip operacije
payloadJSON stringPodaci za slanje
created_attimestampRedoslijed
retry_countintBackoff
last_errorstringDebugging

Kad korisnik uređuje task offline:

  1. 1
    odmah ažurirajte lokalni redak tasks
  2. 2
    enqueueajte outbox event
  3. 3
    označite redak kao dirty = true

5.4 Sync worker: retry s backoffom#

Držite jednostavno:

  • pokrenite na startu aplikacije
  • pokrenite kad se veza vrati
  • pokrenite periodično dok je aplikacija aktivna

Okvir pseudo-implementacije:

Dart
Future<void> syncOutbox() async {
  final events = await localDb.outbox.getPending(limit: 50);
 
  for (final e in events) {
    try {
      await pushEventToSupabase(e);
      await localDb.outbox.markDone(e.id);
      await localDb.entities.clearDirty(e.entityId);
    } catch (err) {
      await localDb.outbox.markFailed(e.id, err.toString());
    }
  }
}

5.5 Sigurno slanje promjena u Supabase#

Za update, uključite version da ne prepišete novije podatke. Jedan pristup:

  • klijent šalje version
  • server primjenjuje update samo ako se verzija poklapa
  • server povećava verziju

Ovo je optimistic concurrency control.

Na serveru to možete enforceati s RPC-om ili uvjetnim updateom.

Ako koristite uvjetni update iz Fluttera, update treba ciljati redak i očekivanu verziju.

Dart
Future<void> updateTaskWithVersion(String id, int expectedVersion, Map<String, dynamic> patch) async {
  final update = {
    ...patch,
    'client_updated_at': DateTime.now().toUtc().toIso8601String(),
  };
 
  final res = await supabase
      .from('tasks')
      .update(update)
      .eq('id', id)
      .eq('version', expectedVersion)
      .select('id, version')
      .maybeSingle();
 
  if (res == null) {
    throw Exception('Conflict: version mismatch');
  }
}

Zatim u sync workeru riješite konflikte:

  • dohvatite najnoviji redak sa servera
  • usporedite s lokalnim “dirty” retkom
  • primijenite strategiju: last-write-wins, merge polja ili prompt korisniku

5.6 Učinkovito povlačenje server promjena#

Ne refetchajte sve na svakom syncu. Koristite updated_at watermarke.

Spremite per-tablicu checkpoint lokalno:

CheckpointPrimjerZnačenje
tasks_last_sync2026-06-02T10:12:00ZZadnji uspješan pull

Zatim povucite updateove:

  • fetchajte retke s updated_at većim od checkpointa
  • lokalno primijenite upsertove
  • pomaknite checkpoint na max updated_at koji je vraćen
Dart
Future<List<Map<String, dynamic>>> fetchTasksSince(String teamId, String sinceIso) async {
  return await supabase
      .from('tasks')
      .select('id, team_id, title, updated_at, deleted_at, version')
      .eq('team_id', teamId)
      .gt('updated_at', sinceIso)
      .order('updated_at');
}

5.7 Soft delete i tombstoneovi#

Hard delete razbija offline usklađivanje jer drugi uređaji mogu propustiti delete event. Preferirajte soft delete:

  • postavite deleted_at
  • po defaultu izbacite obrisane retke iz upita
  • po potrebi periodično purgajte obrisane retke server-side

U Flutteru delete tretirajte kao:

  • lokalno označite redak s deleted_at = now i enqueueajte outbox delete event
  • odmah ga sakrijte iz UI-ja
  • syncajte kasnije

# 6) Sve na jednom mjestu: produkcijski data access stack#

Stabilan obrazac za Flutter Supabase auth realtime offline sync izgleda ovako:

6.1 Repository pattern s local-first čitanjima#

Pravila:

  • UI čita iz streamova lokalne baze
  • repository izlaže watch metode koje vraćaju streamove
  • remote fetch ažurira lokalnu bazu, ne UI direktno
Tip metodeIzvorPrimjer
watchTasks(teamId)Lokalna DBUI lista
refreshTasks(teamId)Remote pa lokalnoPull-to-refresh
editTask(task)Lokalno pa outboxTrenutne izmjene
sync()Outbox pa pullBackground worker

6.2 Točka integracije za Realtime#

Realtime treba pozvati repository da patcha lokalnu bazu.

Primjer ponašanja na event:

  • ako je payload delete s deleted_at, lokalno označite kao obrisano
  • inače upsertajte redak lokalno
  • nemojte automatski navigirati ili prikazivati snackbare, neka UI odluči

6.3 Preporučene biblioteke#

ConcernPreporukaZašto
State managementRiverpodTestabilnost, scoping, async ergonomija
Lokalna DBdrift ili isarZrele, provjerene u produkciji
Connectivityconnectivity_plusDetekcija mrežnih promjena
Background radworkmanager (Android), background_fetch (iOS limits)Best-effort background sync
Loggingtalker ili loggerDebugging sync i auth problema

💡 Savjet: Napravite “Sync Debug” ekran u debug buildovima. Prikažite veličinu outboxa, vrijeme zadnjeg synca, zadnju grešku i trenutni user id. Ovo drastično smanjuje vrijeme debugginga u produkciji.

# 7) Česte produkcijske zamke (i kako ih izbjeći)#

7.1 Korištenje service role ključa u aplikaciji#

Nikad ne isporučujte service role ključ klijentima. Koristite samo anon key.

Ako trebate privilegirane operacije:

  • koristite Supabase Edge Functions
  • ili server-side API pod vašom kontrolom
  • držite RLS policyje stroge

7.2 Realtime djeluje nepouzdano#

Većina problema tipa “realtime je pokvaren” svodi se na jedno od ovoga:

  • RLS sprječava korisnika da vidi redak
  • korisnik je pretplaćen na krivu shemu ili tablicu
  • filter se ne poklapa, posebno pogrešni tipovi stupaca
  • channel nije unsubscriban, više pretplata duplicira evente

Riješite logiranjem:

  • naziva channela
  • statusa pretplate
  • payload id-jeva i team id-jeva
  • trenutnog auth user id-ja

7.3 Bugovi s vremenskim zonama i redoslijedom#

Koristite UTC svugdje:

  • šaljite DateTime.now().toUtc()
  • na serveru spremite timestamptz
  • za sync sortirajte po updated_at, ne po client_updated_at

7.4 Offline kreiranja i “nestali redci”#

Ako kreirate redak offline i odmah ga pokažete, osigurajte da ima UUID i da je prvo upisan u lokalnu bazu. Kod synca ga ubacite u Supabase s istim ID-jem kako biste izbjegli duplikate.

7.5 Prevelika potrošnja bandwidtha#

Lako možete “spaliti” mobilne podatke ako:

  • prečesto pollate
  • refetchate cijele liste nakon svake promjene
  • pretplaćujete se preširoko

Koristite:

  • inkrementalne pullove s updated_at
  • realtime kao patching, ne kao trigger za refetch
  • paginaciju i server-side filtere

# 8) Checklist za testiranje autha, RLS-a, realtimea i offlinea#

Ove scenarije trebate testirati prije releasea:

ScenarijOčekivano ponašanjeKako testirati
Token refreshKorisnik ostaje ulogiranPričekajte 1 sat, pošaljite app u pozadinu, otvorite ponovno
Pokušaj RLS bypassaForbiddenPozovite REST s drugim user id-jem
Offline uređivanjeUI se odmah ažurira, sync je queuedAirplane mode pa uređivanje
KonfliktDeterminističko rješenjeUredite isti redak na dva uređaja
Realtime updateDrugi uređaj primi patchDva uređaja u istom timu
LogoutLokalni podaci se sigurno obrađujuLogout i u offline modu

ℹ️ Napomena: RLS testovi trebaju uključivati direktne API pozive koristeći anon key, ne samo in-app tokove. Model napada je “netko direktno poziva vaše Supabase endpointove”.

# Ključne poruke#

  • Tretirajte Flutter Supabase auth realtime offline sync kao četiri odvojene brige: auth, RLS, realtime i sync, a zatim ih integrirajte kroz repozitorije i lokalnu bazu.
  • Enforceajte dozvole u PostgreSQL-u s RLS-om, postavite default user_id na auth.uid(), i nikad se ne oslanjajte na klijentsku autorizaciju.
  • Koristite realtime usko i s filterima, pretplatite se samo dok su ekrani aktivni i primjenjujte evente na lokalni cache umjesto rebuildanja UI-ja iz mrežnih payloadova.
  • Implementirajte offline-first UX uz lokalnu bazu plus outbox queue, zatim syncajte s inkrementalnim pullovima koristeći updated_at checkpointove.
  • Dodajte optimistic concurrency temeljenu na verziji kako biste detektirali konflikte i riješili ih definiranom strategijom, a ne nagađanjem.
  • Rano izgradite alate za debugging: outbox inspektor, sync logove i vidljivost auth stanja kako biste smanjili vrijeme produkcijskih incidenata.

# Zaključak#

Flutter i Supabase mogu biti snažna produkcijska kombinacija kada od prvog dana dizajnirate za nepouzdane klijente, povremenu povezanost i konkurentne izmjene s više uređaja. Krenite zaključavanjem RLS-a, zatim izgradite local-first repository sloj s outboxom i inkrementalnim syncom, i na kraju dodajte realtime kao mehanizam invalidacije cachea i patchanja.

Ako želite da Samioda pregleda vaše Supabase RLS policyje, implementira offline-first sync sloj ili isporuči produkcijski spremnu Flutter arhitekturu, kontaktirajte nas preko naše stranice i podijelite trenutnu shemu i user flowove.

FAQ

Share
A
Adrijan OmićevićSamioda Team
All articles →

Više iz kategorije Mobilni razvoj

Sve
·14 min čitanja

Skaliranje Fluttera modularizacijom: Postavljanje monorepa s Melosom, dijeljenim paketima i čistim granicama

Praktični vodič za modularizaciju Flutter monorepa s Melosom: kada razdvajati u pakete, kako strukturirati dijeljeni kod, kako provoditi granice te kako učinkovito pokretati CI i testove kroz rastuću bazu koda.

FlutterMonorepoMelosModularizationCI/CDTestingArchitecture
Adrijan OmićevićPročitaj članak
·13 min čitanja

Flutter vs izvorni iOS/Android u 2026.: kompromisi između troška, performansi i vremena do izlaska na tržište

Praktična, brojkama potkrijepljena usporedba Fluttera i izvornog iOS-a i Androida za 2026. — uključuje model troška, realnost performansi, utjecaj održavanja i okvir za odluku za MVP-ove, UI visokih performansi, zahtjevne platform API-je i regulirane aplikacije.

FlutteriOSAndroidRazvoj mobilnih aplikacijaUsporedbaVrijeme do izlaska na tržišteTrošak aplikacije
Adrijan OmićevićPročitaj članak
·13 min čitanja

Vodič za Flutter deep linking za 2026.: Universal Links, Android App Links i pouzdano rutiranje unutar aplikacije

Praktičan, produkcijski spreman vodič za Flutter deep linking: Universal Links, Android App Links, go_router obrada ruta, deferred deep linkovi, osnove atribucije u analitici i kontrolna lista za rješavanje problema.

FlutterRazvoj mobilnih aplikacijaDeep linkingUniversal LinksAndroid App Linksgo_routerAnalitika
Adrijan OmićevićPročitaj članak

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.

Povezani članci