Changer le thème

Supabase Storage en pratique : upload, contrôle d'accès et accélération CDN

Easton editorial illustration: orchestration hub with branches

Vous fixez le message d’erreur dans la console. La fonction d’upload d’avatar est en ligne depuis une demi-heure, et des utilisateurs signalent déjà que tout le monde affiche le même avatar.

Après investigation, le problème venait des politiques RLS de Storage — elles n’étaient tout simplement pas configurées. Le bucket était public, les chemins d’upload n’isolaient pas les utilisateurs, et n’importe qui pouvait écraser les fichiers des autres. Une négligence sur les permissions a failli provoquer un incident en production.

Supabase Storage paraît simple, mais pour l’exploiter vraiment — contrôle d’accès, accélération CDN, transformation d’images — les pièges sont nombreux. Cet article rassemble les erreurs que nous avons rencontrées et les pratiques qui fonctionnent.

1. Démarrage rapide : upload standard

Commençons par le plus basique : envoyer un fichier.

Créer un bucket

Ouvrez la console Supabase, menu de gauche Storage, puis New bucket. Donnez un nom au bucket, par exemple avatars pour les avatars ou posts pour les images d’articles. Une option vous demande Make this bucket public? — ne cochez pas trop vite ; le chapitre sur les permissions l’explique en détail.

Notre habitude : bucket privé pour les fichiers sensibles, bucket public pour les ressources statiques. Par défaut, créez d’abord un bucket privé et ajustez ensuite si besoin.

Code d’upload avec le SDK

En supposant que @supabase/supabase-js est installé, le code reste simple :

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
)

// Upload de fichier
async function uploadFile(file: File) {
  const filePath = `uploads/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('avatars')  // nom du bucket
    .upload(filePath, file, {
      cacheControl: '3600',  // cache 1 heure
      upsert: false  // erreur si le fichier existe déjà, pas d'écrasement
    })

  if (error) {
    console.error('Échec de l\'upload:', error.message)
    return null
  }

  return data.path  // chemin du fichier
}

Honnêtement, ce bloc a été recopié plus de dix fois. Le point clé est la conception de filePath — nous verrons pourquoi un préfixe horodaté aide, et comment isoler par utilisateur.

Limites de taille

La documentation officielle indique un maximum de 5 Go en upload standard. En pratique, en dessous de 6 Mo l’upload standard convient le mieux ; au-delà, préférez la reprise TUS.

TUS, en bref, c’est la reprise d’upload pour les gros fichiers. La connexion tombe, vous reprenez là où vous vous étiez arrêté. Pour vidéos et grandes images, l’écart est énorme — imaginez 90 % de progression perdue sans TUS.

Activer TUS demande une configuration supplémentaire ; si vous n’en avez pas besoin tout de suite, l’upload standard couvre la plupart des cas.

// Exemple TUS (recommandé pour gros fichiers)
const { data, error } = await supabase.storage
  .from('videos')
  .upload('large-video.mp4', file, {
    duplex: 'half',  // upload en flux
    // TUS gère la reprise automatiquement
  })

2. Configuration sécurisée : politiques RLS en détail

Revenons au piège de 3 h du matin : pas de permissions, fichiers écrasables par n’importe qui.

Le Storage Supabase repose comme la base de données sur PostgreSQL. Le contrôle d’accès passe donc par RLS (Row Level Security). Un bucket équivaut à une table, chaque fichier à une ligne.

Bucket public ou privé

À la création, vous choisissez Public bucket ou Private bucket.

Bucket public : lecture sans authentification. Adapté aux avatars publics, logos de site, ressources statiques ouvertes.

Bucket privé : accès soumis à l’authentification. Attention : l’authentification n’est qu’une porte d’entrée ; qui peut lire ou écrire dépend encore des politiques RLS.

Notre conseil : sauf si le fichier est vraiment public, créez un bucket privé par défaut. Mieux vaut ouvrir après configuration qu’essayer de refermer un bucket déjà exposé.

Types de politiques RLS

Sur la page Policies du Storage, quatre opérations :

  • SELECT : lecture (téléchargement, obtention d’URL)
  • INSERT : upload de nouveaux fichiers
  • UPDATE : mise à jour ou écrasement
  • DELETE : suppression

Chaque opération peut avoir sa propre politique. Configuration fréquente :

-- L'utilisateur ne gère que ses propres fichiers
CREATE POLICY "Users manage own files"
ON storage.objects FOR ALL
USING (auth.uid()::text = (storage.foldername(name))[1]);

Ce SQL paraît dense ; décomposons :

  • auth.uid() renvoie l’ID de l’utilisateur connecté
  • storage.foldername(name) extrait le premier segment du chemin
  • pour user123/avatar.jpg, le premier segment est user123

La logique : l’utilisateur n’agit sur le fichier que si le premier segment du chemin correspond à son ID. C’est le cœur de l’isolation par utilisateur.

Mise en œuvre de l’isolation

À l’upload, placez l’ID utilisateur en premier niveau :

async function uploadAvatar(userId: string, file: File) {
  // Chemin : ID utilisateur / nom de fichier
  const filePath = `${userId}/avatar-${Date.now()}.jpg`

  const { data, error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file)

  return data?.path
}

Chaque utilisateur a ainsi son « dossier ». La politique RLS limite les opérations aux chemins commençant par son ID ; les fichiers des autres restent hors de portée.

URL signées pour l’accès

Sur un bucket privé, l’accès direct renvoie 404. Il faut une URL signée :

// Lien temporaire (validité 1 heure)
const { data, error } = await supabase.storage
  .from('avatars')
  .createSignedUrl('user123/avatar.jpg', 3600)

console.log(data?.signedUrl)  // URL complète signée

Ajustez la durée : trop longue, risque de sécurité ; trop courte, mauvaise UX. En général, 1 à 4 heures convient.

Pour un fichier public sans changer le type de bucket, utilisez getPublicUrl :

const { data } = supabase.storage
  .from('public-assets')
  .getPublicUrl('logo.png')

// URL accessible sans signature

Pièges fréquents des politiques

Quelques erreurs déjà vues :

  1. Politique INSERT oubliée : l’utilisateur se connecte mais ne peut pas uploader. Message typique : « new row violates row-level security policy »

  2. Politique trop permissive : USING (true) autorise tout le monde sur tout — équivalent à l’absence de RLS.

  3. Mauvaise structure de chemin : si l’ID n’est pas au premier niveau, foldername ne renvoie pas l’ID. Exemple raté : uploads/user123/file.jpg donne uploads comme premier segment, et la politique échoue.

Testez d’abord dans l’éditeur SQL de la console, validez la logique, puis déployez en production.

3. Performance : Smart CDN et transformation d’images

Upload et permissions en place : comment accélérer le chargement ?

Principe du Smart CDN

Le Smart CDN Supabase n’est pas un CDN classique. Il adapte la durée de cache selon la fréquence d’accès : fichiers populaires plus longtemps, fichiers rares plus court.

La doc indique une synchronisation mondiale de l’invalidation en 60 secondes maximum. Un fichier mis à jour à Tokyo est visible à New York en moins d’une minute — bien plus rapide que plusieurs minutes, voire heures, sur certains CDN traditionnels.

Smart CDN est payant : Pro Plan à 25 $/mois. En Free Plan, les fichiers restent accessibles mais sans accélération CDN, lecture directe depuis les serveurs Supabase.

Paramètres de transformation d’images

Fonction très pratique : redimensionnement et recadrage via des paramètres d’URL, sans pipeline maison.

Paramètres de base :

?width=300&height=200  // dimensions
?resize=contain        // conserver le ratio, sans recadrage
?resize=cover          // remplir la zone, recadrer l'excédent
?quality=80            // qualité (1-100)
?format=webp           // conversion WebP, fichier plus léger

Combinaison :

const baseUrl = supabase.storage
  .from('avatars')
  .getPublicUrl('user123/avatar.jpg').data.publicUrl

// Miniature
const thumbnailUrl = `${baseUrl}?width=100&height=100&resize=cover`

Limites :

  • dimensions : 1 à 2500 pixels
  • fichier source : maximum 25 Mo
  • formats : JPEG, PNG, WebP, GIF, AVIF

Au-delà, erreur. Une fois, une image source de 30 Mo a été refusée pour transformation.

Facturation : quota gratuit par projet

La transformation est facturée au nombre de conversions, pas au stockage.

100 transformations gratuites par projet et par mois. Au-delà : 5 $ pour 1 000 images.

Pour un projet personnel ou une petite équipe, 100 suffit souvent. Sur notre blog, quelques dizaines d’avatars et d’illustrations par mois. Sauf application type réseau social d’images, le coût reste modeste.

Intégration Next.js : Image Loader

Avec Next.js, configurez le loader Supabase pour que next/image applique automatiquement les paramètres :

// next.config.js
module.exports = {
  images: {
    loader: 'custom',
    loaderFile: './supabase-image-loader.js',
  }
}

Fichier loader :

// supabase-image-loader.js
export default function supabaseLoader({ src, width, quality }) {
  const params = new URLSearchParams()
  params.set('width', width.toString())
  params.set('quality', (quality || 75).toString())
  params.set('format', 'webp')

  return `${src}?${params.toString()}`
}

Ainsi <Image src="..." width={300} /> ajoute les paramètres de transformation automatiquement.

Seuil Pro Plan

Smart CDN et transformation d’images exigent le Pro Plan. Le Free Plan se limite à l’upload et au téléchargement de base.

Faut-il upgrader ? Selon le besoin. Quelques avatars : Free Plan suffit. Volume d’images et optimisation perf : le Pro évite de monter son propre CDN et son service de traitement d’images.

Notre approche : Free Plan pour les tests avant mise en ligne, Pro une fois le trafic stabilisé. 25 $/mois, ce n’est pas négligeable.

4. Cas pratique : configuration complète d’un blog

Plutôt que la théorie seule, voici la configuration Storage de notre blog, de zéro à opérationnel.

Scénario : avatars + images d’articles

Deux buckets :

  • avatars : avatars utilisateurs, bucket privé, chacun ne gère que le sien
  • post-images : illustrations d’articles, bucket privé, upload auteur, lecture via URL signée

Étape 1 : créer les buckets

Dans la console :

  1. Storage > New bucket > nom avatars, cocher Private
  2. Même procédure pour post-images

Étape 2 : configurer les politiques RLS

Politiques pour avatars :

-- Lecture de tous les avatars (lecture publique)
CREATE POLICY "Anyone can view avatars"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');

-- Upload et mise à jour de son propre avatar
CREATE POLICY "Users manage own avatar"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);

-- Suppression de son propre avatar uniquement
CREATE POLICY "Users delete own avatar"
ON storage.objects FOR DELETE
USING (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);

Politiques pour post-images :

-- Les auteurs peuvent uploader (rôle author dans le JWT)
CREATE POLICY "Authors can upload post images"
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'post-images'
  AND auth.jwt() ->> 'role' = 'author'
);

-- Lecture publique des images d'articles
CREATE POLICY "Public read post images"
ON storage.objects FOR SELECT
USING (bucket_id = 'post-images');

Étape 3 : code d’upload côté client

Composant d’upload d’avatar :

async function handleAvatarUpload(file: File) {
  const user = await supabase.auth.getUser()
  if (!user.data.user) return alert('Veuillez vous connecter')

  // Chemin : ID utilisateur / avatar.jpg (nom fixe, upsert écrase l'ancien)
  const filePath = `${user.data.user.id}/avatar.jpg`

  const { error } = await supabase.storage
    .from('avatars')
    .upload(filePath, file, { upsert: true })

  if (!error) {
    // URL publique (SELECT autorise la lecture pour tous)
    const url = supabase.storage.from('avatars').getPublicUrl(filePath)
    setUserAvatar(url.data.publicUrl)
  }
}

Upload d’image d’article :

async function handlePostImageUpload(file: File) {
  const filePath = `posts/${Date.now()}-${file.name}`

  const { data, error } = await supabase.storage
    .from('post-images')
    .upload(filePath, file)

  if (!error) {
    // URL signée, validité 24 heures
    const { data: urlData } = await supabase.storage
      .from('post-images')
      .createSignedUrl(filePath, 86400)

    insertImageToEditor(urlData?.signedUrl)
  }
}

Étape 4 : tests avant mise en ligne

Vérifiez :

  1. Un visiteur non connecté voit-il les images d’articles ? (oui, si SELECT le permet)
  2. Un utilisateur standard peut-il uploader une image d’article ? (non, sauf rôle author)
  3. L’utilisateur A peut-il écraser l’avatar de B ? (non, grâce à l’isolation par chemin)

Testez chaque point. La leçon de 3 h du matin, une fois suffit.

Synthèse

Trois piliers pour Supabase Storage : upload, permissions, accélération.

L’upload est le plus simple, quelques lignes de code. Les politiques RLS méritent plus de temps : elles ne se configurent pas une fois pour toutes, il faut les aligner sur le métier et les retester. CDN et transformation d’images sont un plus, réservés au Pro Plan, mais ils font gagner du temps de développement.

Notre approche : d’abord upload et permissions solides, sans faille de sécurité. Ensuite CDN si la perf l’exige, transformation si le produit en a besoin. Avancez par étapes.

Si vous utilisez aussi Supabase Storage, partagez vos galères. L’histoire de 3 h du matin n’est probablement pas unique à nous.

Configuration complète de Supabase Storage

Guide pratique de la création du bucket aux permissions et à l'accélération CDN

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Créer un bucket

    Créez un bucket privé dans la console Supabase :

    • Allez dans Storage > New bucket
    • Saisissez un nom (par ex. avatars)
    • Cochez Private (privé par défaut recommandé)
    • Cliquez sur Create bucket
  2. 2

    Step 2: Configurer les politiques RLS

    Configurez Row Level Security pour le bucket :

    • Allez dans Storage > sélectionnez le bucket > Policies
    • Cliquez sur New Policy
    • Choisissez le type d'opération (SELECT/INSERT/UPDATE/DELETE)
    • Rédigez la règle (par ex. isolation par utilisateur)
    • Testez puis appliquez en production
  3. 3

    Step 3: Uploader des fichiers

    Utilisez le SDK pour l'upload :

    • Concevez la structure des chemins (par ex. userId/filename)
    • Appelez storage.from().upload()
    • Définissez cacheControl et upsert
    • Gérez les erreurs et le chemin renvoyé
  4. 4

    Step 4: Configurer CDN et transformation d'images (optionnel)

    Après passage au Pro Plan :

    • Smart CDN met en cache automatiquement les fichiers populaires
    • Paramètres URL de transformation (width/height/format)
    • Intégration Next.js Image Loader
    • Surveillez le quota gratuit (100 images/mois)

FAQ

Quelle différence entre un bucket public et un bucket privé ?
Un bucket public est lisible par tous, adapté aux ressources statiques ouvertes (logo, avatar public). Un bucket privé exige une authentification ; les droits précis dépendent des politiques RLS, ce qui est plus sûr.
Comment configurer une politique RLS pour isoler les utilisateurs ?
Placez l'ID utilisateur en premier niveau du chemin :

• Chemin d'upload : userId/filename
• Politique : auth.uid()::text = (storage.foldername(name))[1]
• Chaque utilisateur ne peut agir que sur les chemins commençant par son ID
Comment partager un fichier d'un bucket privé ?
Utilisez createSignedUrl() pour générer une URL temporaire signée. Une durée trop longue est risquée, trop courte dégrade l'expérience ; 1 à 4 heures est un bon compromis.
Quelles limites pour l'upload de fichiers ?
L'upload standard accepte jusqu'à 5 Go. En dessous de 6 Mo, l'upload standard offre la meilleure expérience ; au-delà, préférez la reprise TUS pour reprendre après une coupure réseau.
Quels paramètres et limites pour la transformation d'images ?
Paramètres pris en charge :

• width/height : dimensions (1 à 2500 pixels)
• resize : contain (conserver le ratio) ou cover (recadrer pour remplir)
• quality : qualité (1-100)
• format : webp/jpeg/png/gif/avif

Limite : fichier source inférieur à 25 Mo
Smart CDN et transformation d'images sont-ils payants ?
Oui, ils nécessitent le Pro Plan (25 $/mois). Le Free Plan ne couvre que l'upload et le téléchargement de base. La transformation d'images inclut 100 images gratuites par projet et par mois, puis 5 $ pour 1 000 images supplémentaires.

9 min de lecture · Publié le: 9 avr. 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog