# Š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#
| Zahtjev | Verzija | Napomene |
|---|---|---|
| Flutter | 3.19+ | Preporučeno stable |
| Dart | 3.3+ | Dolazi uz Flutter |
| Supabase projekt | Bilo koji | Uključite Email auth i Realtime |
| supabase_flutter | Najnoviji | Službeni klijent za Flutter |
| Local storage | drift ili isar | Za offline cache i outbox |
| Secure storage | flutter_secure_storage | Za tokene i osjetljive zastavice |
ℹ️ Napomena: Supabase Auth sesije
supabase_fluttersprema (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:
idUUID primarni ključuser_idUUID vlasnikcreated_at,updated_atdeleted_atnullable timestamp za soft deleteversioninteger za detekciju konflikataclient_updated_attimestamp koji postavlja klijent, koristi se za usklađivanje
U PostgreSQL terminima:
| Stupac | Tip | Zašto |
|---|---|---|
| id | uuid | Stabilan ID za zapise kreirane offline |
| user_id | uuid | RLS provjere vlasništva |
| updated_at | timestamptz | Server-side izvor istine za sortiranje |
| client_updated_at | timestamptz | Pomaže kad se offline izmjene događaju na više uređaja |
| version | int | Detektira izgubljena ažuriranja, podržava strategije spajanja (merge) |
| deleted_at | timestamptz 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#
// 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.
flutter run \
--dart-define=SUPABASE_URL=your-url \
--dart-define=SUPABASE_ANON_KEY=your-anon-key2.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
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:
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.
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.
| Sloj | Odgovornost | Treba znati za Supabase client |
|---|---|---|
| UI | Forme, rutiranje, view state | Ne |
| AuthRepository | Sign-in/out, session stream | Da |
| Use caseovi | Orkestracija “SignIn”, “SignOut” | Ne |
| Data sourceovi | Supabase API pozivi | Da |
# 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:
- 1uključite RLS
- 2napravite eksplicitne policyje za select, insert, update, delete
U SQL-u:
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.
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:
| Tablica | Ključni stupci | Svrha |
|---|---|---|
| teams | id, created_by | Entitet tima |
| team_members | team_id, user_id, role | Članstvo i uloga |
| tasks | id, team_id, created_by | Sadržaj u okviru tima |
Ideja policyja: dopusti pristup ako je korisnik član tima.
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
Zaboravljanje
with checkna insert i update
usingkontrolira koji su redci vidljivi ili “ciljivi”, dokwith checkkontrolira koji su novi podaci dopušteni. - 2
Pogrešno korištenje security definer funkcija
Ako radite RPC funkcije, razumite zaobilaze li RLS. Držite ih minimalnima i auditanima. - 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.
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 DB | Prednost | Kada odabrati |
|---|---|---|
| drift (SQLite) | Snažni upiti, migracije, relacijsko modeliranje | Business aplikacije s kompleksnim filterima |
| isar | Brz object store, jednostavna perzistencija | Jednostavnije 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:
| Polje | Primjer | Svrha |
|---|---|---|
| id | UUID | Jedinstveni ID eventa |
| entity | "tasks" | Koja tablica |
| entity_id | UUID | Koji redak |
| op | "insert" or "update" or "delete" | Tip operacije |
| payload | JSON string | Podaci za slanje |
| created_at | timestamp | Redoslijed |
| retry_count | int | Backoff |
| last_error | string | Debugging |
Kad korisnik uređuje task offline:
- 1odmah ažurirajte lokalni redak
tasks - 2enqueueajte outbox event
- 3označ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:
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.
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:
| Checkpoint | Primjer | Značenje |
|---|---|---|
| tasks_last_sync | 2026-06-02T10:12:00Z | Zadnji uspješan pull |
Zatim povucite updateove:
- fetchajte retke s
updated_atvećim od checkpointa - lokalno primijenite upsertove
- pomaknite checkpoint na max
updated_atkoji je vraćen
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 = nowi 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
watchmetode koje vraćaju streamove - remote fetch ažurira lokalnu bazu, ne UI direktno
| Tip metode | Izvor | Primjer |
|---|---|---|
watchTasks(teamId) | Lokalna DB | UI lista |
refreshTasks(teamId) | Remote pa lokalno | Pull-to-refresh |
editTask(task) | Lokalno pa outbox | Trenutne izmjene |
sync() | Outbox pa pull | Background 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#
| Concern | Preporuka | Zašto |
|---|---|---|
| State management | Riverpod | Testabilnost, scoping, async ergonomija |
| Lokalna DB | drift ili isar | Zrele, provjerene u produkciji |
| Connectivity | connectivity_plus | Detekcija mrežnih promjena |
| Background rad | workmanager (Android), background_fetch (iOS limits) | Best-effort background sync |
| Logging | talker ili logger | Debugging 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 poclient_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:
| Scenarij | Očekivano ponašanje | Kako testirati |
|---|---|---|
| Token refresh | Korisnik ostaje ulogiran | Pričekajte 1 sat, pošaljite app u pozadinu, otvorite ponovno |
| Pokušaj RLS bypassa | Forbidden | Pozovite REST s drugim user id-jem |
| Offline uređivanje | UI se odmah ažurira, sync je queued | Airplane mode pa uređivanje |
| Konflikt | Determinističko rješenje | Uredite isti redak na dva uređaja |
| Realtime update | Drugi uređaj primi patch | Dva uređaja u istom timu |
| Logout | Lokalni podaci se sigurno obrađuju | Logout 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 synckao č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_idnaauth.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_atcheckpointove. - 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
Više iz kategorije Mobilni razvoj
Sve →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.
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.
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.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Flutter + Firebase: Potpuni vodič za 2026. (Auth, Firestore, Functions, Deploy)
Korak-po-korak Flutter Firebase vodič za 2026.: postavite Firebase, dodajte autentifikaciju, izgradite Firestore CRUD, napišite Cloud Functions i deployajte aplikaciju spremnu za produkciju.
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.
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.