ITSkillsCenter
Développement Web

Supabase Storage : upload images, transformations, policies (tutoriel 2026)

4 min de lecture

📍 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

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité