Développement Web

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

13 دقائق للقراءة

📍 Article principal : Supabase 2026 : guide pratique.

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.

Pour utiliser Supabase Storage, vous avez besoin d’un projet Supabase (Cloud free tier 500 Mo gratuit, ou self-hosted via Docker). Le client SDK supabase-js v2 ou Python ou Dart selon votre frontend. Une clé anon publique pour les uploads frontend, et la service_role key gardée côté serveur. Pour un site marchand à Plateau qui prévoit 1-5 Go de photos produits, le free tier ne suffit pas — passez au Pro à 25 USD/mois (8 Go inclus puis 0,021 USD/Go) ou self-hostez sur un VPS.

É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.

Dans le dashboard Supabase, naviguez vers Storage puis Create new bucket. Nommez-le selon l’usage (avatars, product-images, documents). Choisissez Public bucket pour les fichiers accessibles par URL directe (images de site web), ou Private pour les documents sensibles (contrats, factures). Configurez les MIME types autorisés (image/jpeg, image/webp, image/png) pour bloquer les uploads de fichiers exécutables. Définissez la taille max par fichier (5 Mo pour les avatars, 10 Mo pour les photos produits).

É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);

Le SDK supabase-js gère l’upload en quelques lignes : const { data, error } = await supabase.storage.from('avatars').upload('user-123/avatar.png', file). Le file est un objet File du browser FileAPI. Pour un drag-drop, écoutez l’événement onDrop et appelez upload. Le SDK gère automatiquement les retries en cas d’erreur réseau. Pour les uploads très volumineux (>50 Mo), utilisez resumable uploads via @supabase/storage-js v2 qui gère la reprise après coupure réseau.

É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/...

Pour les buckets publics, l’URL est déterministe et stable. Récupérez avec supabase.storage.from('avatars').getPublicUrl('user-123/avatar.png').data.publicUrl. Cette URL est servie par le CDN Supabase (sur Cloudflare en arrière-plan). Pour minimiser les requêtes, mettez en cache l’URL dans votre base données (champ user.avatar_url) plutôt que la regénérer à chaque rendu. Les images sont servies avec un cache HTTP de 1h par défaut.

É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.

Supabase Storage propose des transformations à la volée via les query params ?width=200&height=200&resize=cover. La requête supabase.storage.from('avatars').getPublicUrl('user.png', { transform: { width: 200, height: 200, resize: 'cover' } }) génère une URL avec ces transformations. Le serveur génère et cache l’image transformée. Cette feature économise 50-70 % de bande passante pour les sites avec beaucoup de miniatures, particulièrement pertinent pour le mobile 4G ouest-africain.

É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

Pour les buckets privés, générez une URL signée temporaire avec supabase.storage.from('docs').createSignedUrl('contract.pdf', 3600) qui retourne une URL valide 1 heure. Pour partager un document confidentiel avec un client externe sans créer de compte, c’est le pattern idéal. Vous pouvez aussi générer une URL d’upload signée que le client utilise pour pousser un fichier directement vers Supabase sans transiter par votre API : utile pour réduire la latence et la charge serveur.

É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))
);

Pour uploader 100+ fichiers en une fois (migration ou import en masse), évitez la boucle synchrone qui prend plusieurs minutes. Utilisez Promise.all avec batches de 5-10 uploads en parallèle : await Promise.all(batch.map(file => supabase.storage.from('photos').upload(file.name, file.data))). Sur une connexion fibre à Almadies, cela traite 200 photos en 30-60 secondes contre 5-10 minutes en séquentiel. Gérez les erreurs individuellement pour ne pas perdre tout le batch en cas d’échec d’un fichier.

É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/*.

Pour des performances optimales en Afrique de l’Ouest où Supabase n’a pas de POP local, mettez Cloudflare devant les URLs publiques. Créez un CNAME custom (cdn.example.sn) qui pointe vers le storage Supabase. Cloudflare cache à Lagos, Casablanca, Le Cap les fichiers servis. La latence chute de 200-300 ms à 30-80 ms pour les utilisateurs ouest-africains. Le coût additionnel est nul (Cloudflare Free tier illimité bande passante).

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

Réalités du terrain en Afrique francophone

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.

Pour une PME à Sicap Liberté ou Cocody qui héberge un catalogue de 500 produits avec 3 photos chacun (1500 photos × 200 Ko = 300 Mo), le free tier Supabase couvre largement la phase d’amorçage. Au-delà, la facture passe à 25 USD/mois Pro. Pour rester économe, deux options : compresser plus agressivement les photos avant upload (Squoosh.app ou ImageMagick à 80 % WebP), ou self-héberger un MinIO dédié sur un VPS Hetzner CX22 à 4 500 FCFA/mois pour 40 Go.

Tutoriels frères

Supabase Storage se complète bien avec d’autres briques Supabase. Auth pour gérer qui peut uploader quoi via des policies RLS. Database Postgres pour stocker les métadonnées des fichiers (titre, description, tags). Edge Functions pour traiter les uploads côté serveur (validation, redimensionnement custom, OCR). Cette intégration native évite la dispersion sur 5 services SaaS différents.

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.

Sur le même thème

Pour creuser le stockage objet, voyez nos tutoriels MinIO self-hosted (alternative open-source à Supabase Storage), Cloudflare R2 (storage S3-compatible avec sortie gratuite), Backblaze B2 pour les sauvegardes longue durée, et Litestream pour la réplication SQLite vers S3. Cette panoplie couvre les cas d’usage du stockage cloud moderne avec des compromis prix/performance variés.

Pourquoi Supabase Storage plutot que S3 pour une PME ouest-africaine

Heberger les images d’une app mobile (avatars, photos produits, justificatifs KYC) sur AWS S3 demande de gerer un compte AWS, de configurer IAM, de souscrire un budget en USD et de rajouter un CDN CloudFront pour la latence. Pour une PME a Dakar, Abidjan ou Cotonou qui demarre, Supabase Storage simplifie : c’est inclus dans le projet, l’API est la meme que la base, le CDN edge fait partie du forfait. Le plan Free offre 1 Go de stockage et 2 Go de bande passante mensuelle.

Au-dela, le plan Pro a 25 USD par mois (environ 16 400 FCFA au taux fixe 1 EUR = 655,957 FCFA, conversion via USD/EUR du jour) inclut 100 Go de stockage et 250 Go de transfert. Suffisant pour la majorite des apps de service en phase 1.

Etape 1 : Creer un bucket dans la console Supabase

Connectez-vous a app.supabase.com, ouvrez votre projet, section Storage. Cliquez New bucket. Donnez un nom court en minuscules sans espaces (ex: avatars). Choisissez Public ou Private. Pour des avatars affiches sur le site, Public suffit ; pour des justificatifs KYC, Private obligatoire.

Validez. Le bucket apparait dans la liste avec une icone cadenas si Private. Vous pouvez creer plusieurs buckets dans le meme projet (avatars, products, kyc-docs) avec des policies differentes.

Etape 2 : Definir la taille et le type de fichiers autorises

Dans les Bucket settings, fixez File size limit (ex: 5 MB pour des avatars) et Allowed MIME types (image/jpeg, image/png, image/webp). Toute tentative d’upload hors de ces limites est rejetee cote serveur, donc inutile de bypasser cote client.

Activer ces limites des le depart evite qu’un utilisateur ne tente d’uploader une video de 200 Mo dans le bucket avatars et ne fasse exploser votre quota gratuit en une nuit.

Etape 3 : Installer le SDK et initialiser le client

Dans votre app frontend (React, Vue, Svelte ou vanilla JS), installez le SDK officiel.

npm install @supabase/supabase-js@2

# supabaseClient.js
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
  'https://xyz.supabase.co',
  'eyJhbGciOiJI...'   // anon key publique
)

La cle anon est publique : elle ne donne acces qu’a ce qu’autorisent les policies RLS et Storage. Ne mettez jamais la service_role key cote client.

Etape 4 : Uploader une image depuis un input file

Dans un composant React simple, on capture le fichier choisi et on l’envoie au bucket. Le path inclut l’ID utilisateur pour pouvoir restreindre les permissions plus tard.

async function uploadAvatar(file) {
  const { data: { user } } = await supabase.auth.getUser()
  const path = `${user.id}/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('avatars')
    .upload(path, file, { contentType: file.type })

  if (error) throw error
  return data.path
}

L’option contentType assure que le fichier est servi avec le bon header HTTP. Sans cela, certains navigateurs telechargent l’image au lieu de l’afficher. La fonction retourne le path interne (avatars/123/1700000000-photo.jpg) a stocker dans votre table users.

Etape 5 : Recuperer l’URL publique pour affichage

Pour un bucket Public, l’URL est deterministe et ne necessite pas d’appel reseau supplementaire.

const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(path)

console.log(data.publicUrl)
// https://xyz.supabase.co/storage/v1/object/public/avatars/123/1700-photo.jpg

Vous pouvez utiliser cette URL directement dans une balise img, dans une feuille de style ou dans une notification email. Le CDN edge la met en cache automatiquement, donc pas besoin d’ajouter Cloudflare devant.

Etape 6 : URL signee pour bucket prive

Pour les fichiers sensibles (justificatifs KYC, contrats), generez une URL temporaire valide quelques minutes.

const { data, error } = await supabase.storage
  .from('kyc-docs')
  .createSignedUrl(path, 60) // valide 60 secondes

if (data) window.open(data.signedUrl)

Apres expiration, l’URL renvoie une erreur 403. Pour un usage interne (ex: telecharger un document depuis le back-office), 60 secondes suffisent ; pour un partage email, montez a 3600 (1 heure) en gardant a l’esprit que toute personne ayant le lien y accedera.

Etape 7 : Politique d’acces par utilisateur

Par defaut, le bucket Private interdit tout. On ajoute une policy SQL pour autoriser un utilisateur a uploader uniquement dans son propre dossier.

-- SQL editor Supabase
create policy "Users can upload own avatar"
on storage.objects for insert
to authenticated
with check (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = auth.uid()::text
);

create policy "Users can read own avatar"
on storage.objects for select
to authenticated
using (
  bucket_id = 'avatars'
  and (storage.foldername(name))[1] = auth.uid()::text
);

La fonction storage.foldername extrait le premier segment du path. Combinee avec auth.uid(), elle empeche un utilisateur d’uploader dans le dossier d’un autre. Testez en vous connectant avec deux comptes differents : chacun ne voit et n’ecrit que son dossier.

Etape 8 : Image transformations a la volee

Supabase propose un service de redimensionnement gratuit sur le plan Pro. Au lieu de stocker plusieurs versions, on demande la taille voulue dans l’URL.

const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(path, {
    transform: { width: 200, height: 200, resize: 'cover' }
  })

Le serveur renvoie une version 200×200 mise en cache. Les options resize valent cover, contain ou fill. Format = webp force la conversion (gain de poids 30 % par rapport au JPEG).

Etape 9 : Supprimer un fichier proprement

Quand un utilisateur change d’avatar, on supprime l’ancien pour eviter d’accumuler les fichiers orphelins.

const { error } = await supabase.storage
  .from('avatars')
  .remove([oldPath])

La suppression est immediate et definitive (pas de corbeille). Dans la table users, mettez a jour le champ avatar_url juste apres le succes de l’upload du nouveau, et supprimez l’ancien chemin uniquement apres. Sinon un crash entre les deux operations laisse l’utilisateur sans avatar.

Etape 10 : Surveiller le quota et alertes

Dans Settings, Usage, vous voyez la consommation Storage et Bandwidth en temps reel. Sur le plan Free, depasser 1 Go bloque les nouveaux uploads ; depasser 2 Go de bande passante bloque les downloads.

Pour ne pas etre surpris, branchez une alerte par email a 80 % de chaque quota dans Settings, Billing. Sur le plan Pro, le depassement est facture a l’usage (0,021 USD par Go stocke, 0,09 USD par Go transfere) : moins brutal mais a surveiller.

Voir aussi notre guide Supabase Auth OAuth et notre tutoriel PocketBase hooks Go.

مشاركة