📍 Article principal : Supabase 2026 : guide complet.
Supabase Storage est S3-compatible avec policies SQL et imgproxy intégré pour transformations à la volée. Ce tutoriel détaille upload images e-commerce et profils utilisateur, validé en production.
Prérequis
- Supabase en production avec Auth fonctionnelle.
- SDK Supabase JS dans frontend.
- Niveau : intermédiaire.
- Temps : 1-2h.
Étape 1 — Créer bucket
Studio → Storage → New Bucket :
- Name :
product-images. - Public : OFF (private bucket).
- File size limit : 5 MB.
- Allowed MIME types : image/jpeg, image/png, image/webp.
Étape 2 — Policies bucket
-- Auth users peuvent upload dans leur dossier user-id
CREATE POLICY "users upload to own folder"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'product-images'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Public read pour images publiées
CREATE POLICY "public read product images"
ON storage.objects FOR SELECT
USING (bucket_id = 'product-images');
-- Users peuvent delete leurs propres images
CREATE POLICY "users delete own images"
ON storage.objects FOR DELETE
USING (
bucket_id = 'product-images'
AND auth.uid()::text = (storage.foldername(name))[1]
);
Étape 3 — Upload depuis frontend
const file = event.target.files[0];
const userId = (await supabase.auth.getUser()).data.user.id;
const filename = `${userId}/${Date.now()}-${file.name}`;
const { data, error } = await supabase.storage
.from('product-images')
.upload(filename, file, {
cacheControl: '3600',
upsert: false
});
if (error) console.error(error);
console.log('Uploaded:', data.path);
Étape 4 — Get public URL
const { data } = supabase.storage
.from('product-images')
.getPublicUrl('user-id/timestamp-photo.jpg');
console.log(data.publicUrl);
// https://api.votre-app.com/storage/v1/object/public/product-images/...
Étape 5 — Image transformations (resize/compress)
// Resize 400x300, format webp
const { data } = supabase.storage
.from('product-images')
.getPublicUrl('user-id/photo.jpg', {
transform: {
width: 400,
height: 300,
resize: 'cover',
format: 'webp',
quality: 80
}
});
imgproxy intégré transforme à la volée. Cache CDN 1 heure.
Étape 6 — Signed URLs (privé temporaire)
Pour fichiers privés (factures PDF) :
const { data } = await supabase.storage
.from('invoices')
.createSignedUrl('user-id/invoice-2026-04.pdf', 3600);
// URL valide 1 heure
Étape 7 — Avatar utilisateur
Bucket avatars public :
const file = avatarInput.files[0];
const userId = user.id;
await supabase.storage
.from('avatars')
.upload(`${userId}.jpg`, file, { upsert: true });
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}.jpg`, {
transform: { width: 200, height: 200, resize: 'cover' }
});
// Update profile
await supabase
.from('profiles')
.update({ avatar_url: data.publicUrl })
.eq('id', userId);
Étape 8 — Bulk upload
const files = Array.from(event.target.files);
const uploads = await Promise.all(
files.map(f => supabase.storage
.from('product-images')
.upload(`${userId}/${Date.now()}-${f.name}`, f))
);
Étape 9 — Storage cleanup
Edge function pour purger orphans :
// supabase/functions/storage-cleanup/index.ts
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
Deno.serve(async () => {
const supabase = createClient(...);
const { data: files } = await supabase.storage.from('product-images').list();
for (const file of files) {
const { count } = await supabase
.from('products')
.select('*', { count: 'exact', head: true })
.like('image_url', `%${file.name}%`);
if (count === 0) {
await supabase.storage.from('product-images').remove([file.name]);
}
}
return new Response('cleaned');
});
Étape 10 — CDN Cloudflare devant
Pour cache global et économie bandwidth, Cloudflare devant Supabase storage. Cache rules sur /storage/v1/object/public/*.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Upload 403 | Policy missing | Créer INSERT policy |
| File size limit | 5 MB max default | Augmenter dans bucket settings |
| MIME type rejected | Liste restrictive | Add MIME dans bucket |
| Transformations 404 | imgproxy non démarré | Vérifier service docker |
| Signed URL expirée | Délai trop court | 3600s minimum |
| Storage plein | Pas de cleanup | Edge function cleanup mensuel |
Adaptation au contexte ouest-africain
Trois précisions. Resize obligatoire mobile : photo iPhone 12 MP = 5 MB. Resize 800×600 webp = 80 KB. Économie data 4G x60. CDN Cloudflare : edge cache global, latence Dakar/Abidjan minimum. Storage local : Supabase Storage backend file = sur VPS Hetzner Falkenstein. Souveraineté garantie.
Tutoriels frères
FAQ
S3 externe ? Oui via STORAGE_BACKEND=s3 + AWS_*. Pour scale > 100 GB.
Vidéos lourdes ? Possible mais préférer Mux ou Cloudflare Stream.
Resize quality vs size ? quality=80 webp = équilibre.
Storage Postgres ? Storage utilise Postgres pour metadata + filesystem pour blobs.
Backup storage ? rclone vers Backblaze B2 quotidien.
Pour aller plus loin
- 🔝 Pilier : Guide complet Supabase 2026
- Documentation Storage : supabase.com/docs/guides/storage