# Što ćete izgraditi#
Ovaj vodič pokazuje produkcijski spremnu postavku za Flutter Supabase upload datoteka za privatni korisnički sadržaj: profilne fotografije, račune i slike u chatu. Implementirat ćete upload iz kamere i galerije, promjenu veličine slika na uređaju, otporne pozadinske retry pokušaje te sigurna preuzimanja pomoću kratkotrajnih potpisanih URL-ova.
Također ćete postaviti Supabase Storage politike za write i read pristup, povezati datoteke s redovima u bazi uz RLS i obraditi realne kvarove poput nestabilnih mreža i djelomičnih uploadova.
ℹ️ Napomena: Kontrola pristupa za Supabase Storage provodi se putem Storage politika u Postgresu. RLS na vašim tablicama je odvojena stvar, ali tipično kombinirate oboje kako biste spriječili “puknute” lance autorizacije.
# Preduvjeti#
| Zahtjev | Verzija | Napomene |
|---|---|---|
| Flutter | 3.22+ | Radi na iOS-u i Androidu |
| Dart | 3.4+ | Dolazi s Flutterom |
| Supabase projekt | Najnovije | Storage omogućen |
| supabase_flutter | 2.x | Auth + Storage klijent |
| image_picker | 1.x | Kamera i galerija |
| flutter_image_compress | 2.x | Promjena veličine i kompresija |
| path | 1.x | Operacije nad putanjama |
| sqflite ili hive | Najnovije | Persistiranje upload queuea |
| connectivity_plus | Najnovije | Stanje mreže (opcionalno) |
Ako implementirate i auth, realtime i offline sync obrasce, uparite ovaj post s Flutter + Supabase Auth, Realtime i Offline Sync. Za ojačane mobilne sigurnosne pretpostavke pročitajte Ojačavanje sigurnosti Flutter aplikacije.
# Model podataka i strategija bucketa#
Sigurna arhitektura počinje jasnim razdvajanjem odgovornosti:
- 1Storage bucket drži “sirove” bajtove i provodi pravila na razini objekta.
- 2Postgres tablica prati metapodatke i provodi tko smije referencirati koji objekt.
- 3Potpisani URL-ovi daju vremenski ograničen read pristup bez javnog bucketa.
Praktična početna osnova:
- Naziv bucketa:
user_uploads(privatno) - Putanja objekta:
users/<user_id>/<uuid>.<ext> - Tablica:
mediasowner_id,bucket,path,mime_type,bytes,width,height,created_at
Primjer tablice:
| Stupac | Tip | Svrha |
|---|---|---|
| id | uuid | Primarni ključ |
| owner_id | uuid | auth.uid() |
| bucket | text | U pravilu user_uploads |
| path | text | Storage object key |
| mime_type | text | Za renderiranje i validaciju |
| bytes | bigint | Kvote i prikaz u UI-ju |
| width | int | Optimizacije renderiranja slika |
| height | int | Optimizacije renderiranja slika |
| created_at | timestamptz | Auditiranje |
# Supabase postavljanje: Bucketi, politike i RLS#
1) Kreirajte privatni bucket#
U Supabase Dashboardu:
- Storage → Buckets → New bucket →
user_uploads - Postavite bucket na private
- Opcionalno: uključite restrikcije MIME tipova ako imate poznat skup
Zašto je private bitan: javni bucketi pretvaraju autorizaciju u problem koji rješava samo aplikacija. Potpisani URL-ovi zadržavaju kontrolu pristupa na serveru i vremenski je ograničavaju.
2) Storage politike za upload i čitanje#
Supabase Storage koristi Postgres politike na storage.objects. Tipično dopuštate:
- Insert: samo autentificirani korisnici, i to samo u vlastitu mapu
- Select: samo autentificirani korisnici, i to samo svoju mapu
- Update i delete: isto pravilo kao insert, ako vam treba
Čest obrazac politike provjerava da prvi segment putanje nakon users/ odgovara autentificiranom user id-u.
-- Allow authenticated users to upload only into users/<uid>/...
create policy "User can upload to own folder"
on storage.objects
for insert
to authenticated
with check (
bucket_id = 'user_uploads'
and (storage.foldername(name))[1] = 'users'
and (storage.foldername(name))[2] = auth.uid()::text
);
-- Allow authenticated users to read only their own files
create policy "User can read own files"
on storage.objects
for select
to authenticated
using (
bucket_id = 'user_uploads'
and (storage.foldername(name))[1] = 'users'
and (storage.foldername(name))[2] = auth.uid()::text
);⚠️ Upozorenje: Ako koristite format putanje
users/<uid>/..., pobrinite se da svaki upload s klijenta točno slijedi taj format. Jedno jedino odstupanje izgledat će kao “nasumičan” 403 u produkciji.
3) RLS za tablicu metapodataka#
Uključite RLS na media i dodajte politike temeljene na vlasniku.
alter table public.media enable row level security;
create policy "Media is readable by owner"
on public.media
for select
to authenticated
using (owner_id = auth.uid());
create policy "Media is insertable by owner"
on public.media
for insert
to authenticated
with check (owner_id = auth.uid());
create policy "Media is deletable by owner"
on public.media
for delete
to authenticated
using (owner_id = auth.uid());Ovo osigurava da korisnik može kreirati i dohvaćati metapodatke samo za svoje datoteke, čime se sprječava enumeracija kroz vaš API čak i ako pogode ID-eve.
Zašto trebate i Storage politike i RLS#
Ako zaključate samo Storage, a ne i media, korisnici i dalje mogu dohvatiti metapodatke i saznati putanje, MIME tipove i vremenske oznake. Ako zaključate samo media, a ne Storage, korisnici možda mogu povući “sirove” datoteke direktno. Trebate oba sloja.
# Flutter klijent: Ovisnosti i inicijalizacija#
Dodajte pakete:
flutter pub add supabase_flutter image_picker flutter_image_compress path uuid sqflite connectivity_plusInicijalizirajte Supabase:
// main.dart
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Supabase.initialize(
url: 'https://YOUR_PROJECT.supabase.co',
anonKey: 'YOUR_ANON_KEY',
);
runApp(const MyApp());
}# Upload tok: Kamera i galerija#
Čist UX nudi i snimanje kamerom i odabir iz galerije, a zatim pokreće isti processing pipeline:
- 1Odaberi ili snimi sliku
- 2Validiraj veličinu i tip
- 3Promijeni veličinu i komprimiraj
- 4Upload u Storage
- 5Ubaci (insert) red metapodataka u Postgres
- 6Generiraj potpisani URL za prikaz
Odabir slike#
import 'package:image_picker/image_picker.dart';
final _picker = ImagePicker();
Future<XFile?> pickFromGallery() {
return _picker.pickImage(
source: ImageSource.gallery,
imageQuality: 100,
);
}
Future<XFile?> captureFromCamera() {
return _picker.pickImage(
source: ImageSource.camera,
preferredCameraDevice: CameraDevice.rear,
imageQuality: 100,
);
}💡 Savjet: Ovdje koristite
imageQuality: 100i sami kontrolirajte kompresiju. Oslanjanje na kompresiju iz picker-a daje neujednačen rezultat između uređaja.
# Promjena veličine i kompresija slike (na uređaju)#
Upload fotografija u punoj rezoluciji jedan je od najbržih načina da napuhnete troškove pohrane i bandwidtha. Tipična moderna fotografija s mobitela ima 3 MB do 8 MB i 12 MP do 48 MP. Ako smanjite dulji rub na 1600 px i komprimirate u WebP ili JPEG, često možete smanjiti veličinu za 70% do 90% uz zanemariv utjecaj u UI-ju za avatere i feedove.
Primjer promjene veličine#
import 'dart:io';
import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:path/path.dart' as p;
Future<File> compressForUpload(File input) async {
final dir = input.parent;
final outPath = p.join(
dir.path,
'${p.basenameWithoutExtension(input.path)}_upload.webp',
);
final result = await FlutterImageCompress.compressAndGetFile(
input.path,
outPath,
format: CompressFormat.webp,
quality: 82,
minWidth: 1600,
minHeight: 1600,
);
if (result == null) {
throw Exception('Image compression failed');
}
return File(result.path);
}Produkcijske odluke koje trebate eksplicitno donijeti:
| Postavka | Preporučena početna vrijednost | Zašto je bitno |
|---|---|---|
| Dulji rub | 1280 do 2048 px | Pokriva većinu feedova bez rasipanja |
| Kvaliteta | 75 do 85 | Najbolji kompromis za mobitel |
| Format | WebP na Androidu, JPEG fallback na iOS-u | Kompatibilnost i veličina |
| Maks. upload bajtova | 5 MB do 10 MB | Sprječava zloupotrebu i timeoutove |
# Upload u Supabase Storage#
Kreirajte stabilnu putanju objekta#
Neka putanje budu determinističke i ograničene na user id. Koristite UUID ime datoteke kako biste izbjegli kolizije i curenje informacija.
import 'package:uuid/uuid.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
final supabase = Supabase.instance.client;
final uuid = const Uuid();
String buildObjectPath({
required String userId,
required String extension,
}) {
final id = uuid.v4();
return 'users/$userId/$id.$extension';
}Upload bajtova uz content type#
import 'dart:io';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<String> uploadFile({
required File file,
required String mimeType,
}) async {
final user = supabase.auth.currentUser;
if (user == null) throw Exception('Not authenticated');
final ext = file.path.split('.').last.toLowerCase();
final path = buildObjectPath(userId: user.id, extension: ext);
await supabase.storage
.from('user_uploads')
.upload(
path,
file,
fileOptions: FileOptions(
contentType: mimeType,
upsert: false,
),
);
return path;
}Ubacite red metapodataka nakon uploada#
Storage upload tretirajte kao izvor istine da bajtovi postoje. Metapodatke upisujte tek kad upload uspije.
Future<String> createMediaRow({
required String path,
required String mimeType,
required int bytes,
}) async {
final user = supabase.auth.currentUser;
if (user == null) throw Exception('Not authenticated');
final res = await supabase
.from('media')
.insert({
'owner_id': user.id,
'bucket': 'user_uploads',
'path': path,
'mime_type': mimeType,
'bytes': bytes,
})
.select('id')
.single();
return res['id'] as String;
}🎯 Ključna poruka: Prvo upload, zatim insert metapodataka. Ako prvo upišete metapodatke pa upload padne, stvarate “viseće” zapise koje je teško čistiti i koji mogu curiti u UI.
# Pozadinski retry: Izgradnja upload queuea#
Mobilni uploadovi padaju iz razloga koji se ne vide u lokalnom testiranju: liftovi, tuneli, promjene baznih stanica, captive portali i ubijanje aplikacije zbog nedostatka memorije. Minimalni sloj pouzdanosti je upload queue spremljen lokalno, s retryjem uz eksponencijalni backoff.
Dizajn queuea#
Spremite dovoljno stanja da možete ponoviti upload bez UI konteksta:
| Polje | Tip | Primjer | Zašto |
|---|---|---|---|
| id | text | UUID | Jedinstveni job |
| local_path | text | /data/user/.../tmp.webp | Lokacija datoteke |
| mime_type | text | image/webp | Potrebno za upload |
| status | text | queued ili uploading ili failed | UI i logika |
| attempts | int | 0..N | Backoff |
| next_retry_at | int | epoch ms | Raspoređivanje |
| last_error | text | SocketException | Debugiranje |
Funkcija eksponencijalnog backoffa#
int computeBackoffSeconds(int attempts) {
final base = 2;
final maxSeconds = 300;
final seconds = base * (1 << (attempts.clamp(0, 8)));
return seconds > maxSeconds ? maxSeconds : seconds;
}Skica worker petlje#
Ovo je namjerno jednostavno i radi čak i bez “pravog” background izvršavanja. Možete je pokretati pri startu aplikacije, pri resumeu i nakon promjene konekcije.
Future<void> processQueueOnce() async {
final jobs = await db.fetchDueJobs(limit: 3);
for (final job in jobs) {
try {
await db.markUploading(job.id);
final file = File(job.localPath);
final path = await uploadFile(file: file, mimeType: job.mimeType);
await createMediaRow(
path: path,
mimeType: job.mimeType,
bytes: await file.length(),
);
await db.markDone(job.id);
} catch (e) {
final attempts = job.attempts + 1;
final backoff = computeBackoffSeconds(attempts);
await db.markFailed(
job.id,
attempts: attempts,
nextRetryAt: DateTime.now().add(Duration(seconds: backoff)),
lastError: e.toString(),
);
}
}
}Reality check za background izvršavanje#
iOS snažno ograničava always-on pozadinske zadatke. Android je fleksibilniji, ali i dalje podliježe OEM battery politikama. Praktičan pristup:
- 1Persistirajte queue
- 2Obrađujte ga na otvaranju aplikacije, na resume i nakon povratka konekcije
- 3Za Android-heavy aplikacije razmotrite WorkManager
- 4Držite jobove malima tako da prvo komprimirate
Ako trebate server-managed upload pipeline, pogledajte opći presigned URL obrazac u Next.js Upload Datoteka s Presigned URL-ovima i prilagodite arhitekturu za vaš mobilni backend.
# Sigurna preuzimanja s potpisanim URL-ovima#
Privatni bucketi zahtijevaju potpisane URL-ove za read pristup. Aplikacija zatraži URL koji brzo istječe, pa ga koristi u image widgetu ili HTTP klijentu.
Kreiranje potpisanog URL-a#
Future<String> createSignedUrl({
required String path,
int expiresInSeconds = 60,
}) async {
final res = await supabase.storage
.from('user_uploads')
.createSignedUrl(path, expiresInSeconds);
return res;
}Prikaz slike s kratkotrajnim URL-om#
U praksi biste trebali cacheirati potpisani URL u memoriji za vrijeme trajanja (TTL) kako ne biste ponovno potpisivali pri svakom rebuildanju. Ako koristite biblioteku za cacheiranje slika, provjerite poštuje li query stringove i isteke.
Čest obrazac:
- Dohvatite potpisani URL kada widget postane vidljiv
- Osvježite ako padne s 401 ili 403
- Izbjegavajte duge TTL-ove za osjetljiv medij
⚠️ Upozorenje: Nemojte postavljati istek potpisanog URL-a na sate za privatne korisničke podatke. Ako URL procuri, vrijedi do isteka, a mobilni logovi, proxyji ili crash reportovi ga mogu otkriti.
# Obrasci kontrole pristupa koji izdrže produkciju#
Obrazac A: Datoteke samo za korisnika#
Koristite putanju users/<uid>/... i politike prikazane ranije. Ovo pokriva profilne slike, osobne dokumente i privatne eksportove.
Obrazac B: Dijeljene datoteke, poput chatova ili timova#
Trebate join tablicu koja definira članstvo, a Storage politike je moraju referencirati. Supabase politike mogu upitima dohvaćati iz drugih tablica, pa možete nametnuti da korisnik smije čitati datoteku samo ako je dio chata ili tima koji je posjeduje.
Neka putanja objekta odražava vlasništvo, npr. teams/<team_id>/..., i implementirajte:
team_memberstablicu s RLS-om- Storage select politiku koja provjerava članstvo
mediared u bazi koji se veže na team id i ima RLS temeljen na članstvu
Ovdje kompleksnost autorizacije brzo raste. Ako vaša aplikacija ima stroge zahtjeve privatnosti, uskladite se s praksama iz Ojačavanje sigurnosti Flutter aplikacije i pretpostavite da se klijenti mogu kompromitirati.
# Praktičan error handling: Što hvatati i što prikazati#
Većina upload grešaka spada u mali skup kategorija. Obradite ih eksplicitno kako bi UI ostao predvidljiv.
| Kvar | Simptom | Što napraviti |
|---|---|---|
| Nije autentificiran | 401 ili null user | Forsirajte ponovnu prijavu, pauzirajte queue |
| Politika odbila | 403 | Logirajte putanju i user id, provjerite politiku i shemu imenovanja |
| Mreža ne radi | SocketException | Retry uz backoff |
| Timeout | spor upload | Smanjite sliku, retry |
| Datoteka nedostaje | lokalno čišćenje | Označite kao trajno neuspješno i tražite korisnika da ponovno odabere |
| Duplikat putanje | rijetko s UUID | Regenerirajte i retry |
Mapiranje grešaka u Dart-u#
String userMessageFromError(Object e) {
final msg = e.toString().toLowerCase();
if (msg.contains('not authenticated') || msg.contains('jwt')) {
return 'Please sign in again to upload files.';
}
if (msg.contains('403') || msg.contains('permission')) {
return 'Upload blocked by permissions. Please contact support.';
}
if (msg.contains('socketexception') || msg.contains('network')) {
return 'No internet connection. Upload will retry automatically.';
}
return 'Upload failed. We will retry in the background.';
}# Varijante slika i strategija promjene veličine#
Imate dvije glavne opcije:
Opcija 1: Promjena veličine na uređaju i upload jedne varijante#
Najbolje za jednostavnost i trošak. Odlično radi za avatere i tipične feedove.
- Upload: 1 datoteka
- CDN: manje bajtova
- Server: bez obrade
Opcija 2: Upload originala i generiranje varijanti#
Najbolje kada trebate više veličina ili želite “future-proof” pristup. Varijante možete implementirati kroz automatizacijski workflow koji kreira thumbnailove i sprema ih uz original.
Jednostavna shema imenovanja:
| Varijanta | Primjer putanje | Use case |
|---|---|---|
| original | users/<uid>/<id>.jpg | arhiva |
| thumb | users/<uid>/<id>_thumb.webp | liste |
| medium | users/<uid>/<id>_md.webp | feed |
Ako generirate varijante na serveru, bucket neka ostane private i primijenite ista pravila politika, ili generirajte u zaseban privatni bucket.
💡 Savjet: Ako implementirate server-side resizing, izbjegnite da to radi mobilni klijent. Uploadajte jedan kanonski fajl i neka automatizacija stvara derivate radi konzistentnosti.
# Čišćenje i životni ciklus: Sigurno brisanje datoteka#
Kada korisnici obrišu objavu ili zamijene avatar, uklonite oboje:
- 1
mediared - 2Storage objekt
Redoslijed je važan zbog mogućnosti oporavka. U mnogim aplikacijama prvo brišete DB zapis, pa pokušate obrisati iz Storagea. Ako Storage delete padne, zakažite cleanup job.
Primjer brisanja:
Future<void> deleteMedia({
required String mediaId,
required String path,
}) async {
await supabase.from('media').delete().eq('id', mediaId);
await supabase.storage.from('user_uploads').remove([path]);
}# Checklist za testiranje#
Testirajte pipeline pod stvarnim ograničenjima:
- 1Upload iz kamere i galerije na obje platforme
- 2Uključite airplane mode usred uploada i potvrdite ponašanje retry queuea
- 3Forsirano zatvorite app tijekom uploada i potvrdite da se job nastavlja kasnije
- 4Provjerite da korisnik ne može čitati tuđe datoteke mijenjanjem putanje
- 5Potvrdite da potpisani URL-ovi istječu i da logika osvježavanja radi
- 6Uploadajte najveću realnu fotografiju koju vaši korisnici stvaraju i izmjerite vrijeme
Za širu offline-first arhitekturu pogledajte Flutter + Supabase Auth, Realtime i Offline Sync.
# Ključne poruke#
- Bucket držite private, provodite Storage politike po prefiksu putanje i koristite kratkotrajne potpisane URL-ove za preuzimanje.
- Kombinirajte Storage politike s RLS-om na tablici
mediakako korisnici ne bi mogli pristupiti ni bajtovima ni metapodacima koje ne posjeduju. - Komprimirajte i smanjite slike na uređaju kako biste smanjili upload bajtove za 70% do 90% za tipične mobilne fotografije.
- Implementirajte upload queue s perzistiranim jobovima i eksponencijalnim backoffom da preživite nestabilne mreže i ubijanja aplikacije.
- Prvo upload, zatim insert metapodataka, i eksplicitno obradite česte kvarove kako biste izbjegli “viseće” zapise i zbunjujući UI.
# Zaključak#
Robustan Flutter Supabase tok uploada datoteka uglavnom se svodi na konzistentnost i defense-in-depth: predvidljive putanje objekata, stroge politike, potpisani URL-ovi i retry koji polazi od pretpostavke da će mreža zakazati. Ako želite da Samioda implementira sigurne uploadove, pozadinske retry mehanizme i media pipeline od početka do kraja u vašoj Flutter aplikaciji, kontaktirajte nas i pomoći ćemo vam isporučiti rješenje s produkcijskom kontrolom pristupa i observabilityjem.
FAQ
Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.
Više iz kategorije Mobilni razvoj
Sve →Strategija testiranja u Flutteru: unit, widget, integracijski i golden testovi za brz i pouzdan CI (2026)
Praktična strategija testiranja u Flutteru temeljena na piramidi testiranja: kada koristiti unit, widget, integracijske i golden testove, kako smanjiti flakiness i kako sve pokretati brzo u CI-u.
Flutter navigacija s go_router: duboke poveznice, auth guardovi, ugniježđene rute i podrška za web
Vodič spreman za produkciju za Flutter go_router deep links, uključujući auth preusmjeravanja, ShellRoute layout, ugniježđenu navigaciju, stanje vođeno URL-om i testiranje deep linkova za iOS, Android i web.
Ojačavanje sigurnosti Flutter aplikacije: SSL pinning, detekcija roota i jailbreaka te sigurna pohrana (Vodič za 2026.)
Praktičan vodič za ojačavanje sigurnosti Flutter aplikacije uz checklistu temeljenu na threat modelu, obrasce sigurne pohrane, kompromise SSL pinninga, provjere integriteta u runtimeu i sigurno rukovanje auth tokenima.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Flutter push notifikacije u produkciji: FCM + APNs, deep linkovi i pouzdanost (vodič za 2026.)
End-to-end produkcijski vodič za Flutter push notifikacije uz FCM i APNs: postavljanje, životni ciklus tokena, segmentacija, deep linkovi, obrada u pozadini i pri ugašenoj aplikaciji, te checkliste za pouzdanost i otklanjanje problema.
Flutter + Supabase vs Firebase u 2026: Auth, Realtime, Offline, Cijene i Lock-In
Praktična usporedba za 2026. Fluttera sa Supabaseom i Firebaseom kroz auth, push, realtime, offline/local-first, storage, funkcije, cijene i vendor lock-in — uz preporuke po tipu aplikacije i skali.
Flutter + Supabase u produkciji: Auth, Realtime, RLS i offline-pristup podacima (Vodič za 2026.)
Vodič spreman za produkciju za Flutter + Supabase auth, realtime i offline sync: sigurni auth flowovi, obrasci za Row Level Security, realtime pretplate te offline-first UX uz praktičan kod i česte zamke.