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

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 estuser123
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 :
-
Politique INSERT oubliée : l’utilisateur se connecte mais ne peut pas uploader. Message typique : « new row violates row-level security policy »
-
Politique trop permissive :
USING (true)autorise tout le monde sur tout — équivalent à l’absence de RLS. -
Mauvaise structure de chemin : si l’ID n’est pas au premier niveau,
foldernamene renvoie pas l’ID. Exemple raté :uploads/user123/file.jpgdonneuploadscomme 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 sienpost-images: illustrations d’articles, bucket privé, upload auteur, lecture via URL signée
Étape 1 : créer les buckets
Dans la console :
- Storage > New bucket > nom
avatars, cocher Private - 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 :
- Un visiteur non connecté voit-il les images d’articles ? (oui, si SELECT le permet)
- Un utilisateur standard peut-il uploader une image d’article ? (non, sauf rôle author)
- 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
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
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
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
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é ?
Comment configurer une politique RLS pour isoler les utilisateurs ?
• 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é ?
Quelles limites pour l'upload de fichiers ?
Quels paramètres et limites pour la transformation d'images ?
• 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 ?
9 min de lecture · Publié le: 9 avr. 2026 · Mis à jour le: 27 juil. 2026
Supabase en pratique
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Supabase Auth en pratique : vérification e-mail, OAuth et gestion de session
Guide pratique Supabase Auth : configuration de la vérification e-mail, intégration OAuth, gestion de session JWT et flux PKCE. Tout pour l'authentification utilisateur.
Partie 3 sur 10
Suivant
Supabase Realtime en pratique : trois modes comparés et apps collaboratives
Supabase Realtime propose trois modes : Postgres Changes, Presence et Broadcast. Comparaison des forces de chacun, exemples complets d'app collaborative et configuration RLS sécurisée.
Partie 5 sur 10



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire