L’essentiel en 30 secondes
- Les droits d'accès de Supabase Storage sont des policies RLS sur la table storage.objects : sans policy, aucun envoi n'est autorisé.
- Rangez les fichiers de chaque utilisateur dans un dossier à son identifiant et comparez-le avec storage.foldername(name) dans les policies.
- Les transformations d'images sont réservées au plan Pro et au-dessus ; le WebP est servi automatiquement et format n'accepte que 'origin'.
- Supprimez les fichiers par l'API Storage, jamais par SQL, sinon ils restent orphelins dans le bucket.
Supabase Storage stocke vos fichiers (images, PDF, vidéos) dans des buckets et en décrit chaque objet dans la table Postgres storage.objects. Conséquence pratique : les droits d'accès s'écrivent comme des policies RLS, en SQL, exactement comme pour vos tables. Sans policy, aucun envoi de fichier n'est autorisé1. Le service est aussi compatible avec le protocole S3, ce qui permet d'utiliser la plupart des clients S3 existants2.
Pour la place de Storage dans l'architecture Supabase, voir notre guide de la plateforme Supabase.
Bucket public ou privé : lequel choisir ?
| — | Bucket public | Bucket privé (défaut) |
|---|---|---|
| Lecture | Toute personne qui a l'URL | Soumise aux policies RLS, ou via une URL signée temporaire |
| Envoi, suppression, déplacement | Soumis aux policies | Soumis aux policies |
| Cache CDN | Plus performant | Mis en cache différemment |
| Exemples | Avatars, images d'articles | Factures, documents personnels |
Les buckets sont privés par défaut3.
Comment créer un bucket ?
Créer un bucket est une opération d'administration : faites-la dans une migration SQL, depuis le tableau de bord, ou côté serveur. Les restrictions de type et de taille se définissent à la création, et une requête qui ne les respecte pas est rejetée4.
-- Dans une migration
insert into storage.buckets (id, name, public)
values ('avatars', 'avatars', true);// Côté serveur, avec le client JavaScript
const { data, error } = await supabase.storage.createBucket('avatars', {
public: true,
allowedMimeTypes: ['image/*'],
fileSizeLimit: '1MB',
})La taille maximale par bucket ne peut pas dépasser la limite globale du projet : 50 Mo par fichier sur le plan Free, jusqu'à 500 Go sur Pro et Team5.
Comment écrire les policies d'accès ?
Le modèle le plus courant : chaque utilisateur range ses fichiers dans un dossier qui porte son identifiant. La fonction storage.foldername(name) renvoie les dossiers du chemin ; on compare le premier à l'identifiant de l'utilisateur connecté1. Pour autoriser upsert (écraser un fichier existant), il faut en plus les droits SELECT et UPDATE1.
create policy "avatars : lecture de ses fichiers"
on storage.objects for select to authenticated
using (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));
create policy "avatars : envoi dans son dossier"
on storage.objects for insert to authenticated
with check (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));
create policy "avatars : remplacement de ses fichiers"
on storage.objects for update to authenticated
using (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));
create policy "avatars : suppression de ses fichiers"
on storage.objects for delete to authenticated
using (bucket_id = 'avatars' and (storage.foldername(name))[1] = (select auth.jwt()->>'sub'));Sur un bucket public, la policy SELECT n'est pas nécessaire pour afficher les fichiers ; elle reste utile pour l'upsert. Pour la logique des policies, voir notre article sur la Row Level Security de Supabase.
Comment envoyer un fichier ?
Le chemin doit commencer par l'identifiant de l'utilisateur pour correspondre aux policies ci-dessus :
async function uploadAvatar(file: File, userId: string) {
const ext = file.name.split('.').pop()
const path = `${userId}/avatar.${ext}` // premier dossier = id de l'utilisateur
const { error } = await supabase.storage
.from('avatars')
.upload(path, file, { cacheControl: '3600', upsert: true })
if (error) throw error
const { data } = supabase.storage.from('avatars').getPublicUrl(path)
return data.publicUrl
}Correction : l'ancienne version envoyait le fichier vers avatars/${userId}.ext dans le bucket avatars. Le premier dossier du chemin était donc avatars et non l'identifiant, et la policy fondée sur storage.foldername(name)[1] refusait l'envoi.
Comment redimensionner les images à la volée ?
Les transformations d'images sont réservées aux plans Pro et supérieurs. Elles s'activent ou se coupent dans Storage > Settings6.
const { data } = supabase.storage.from('photos').getPublicUrl('produits/chaise.jpg', {
transform: {
width: 400,
height: 400,
resize: 'cover', // 'cover' (défaut) | 'contain' | 'fill'
quality: 80, // 20 à 100, 80 par défaut
},
})- Format : Storage renvoie automatiquement du WebP aux navigateurs qui le supportent. L'option
formatne sert qu'à désactiver ce comportement (format: 'origin') ; l'ancienne valeurformat: 'webp'de cet article n'existe pas6. - Limites : largeur et hauteur entre 1 et 2 500 pixels, image source de 25 Mo et 50 mégapixels maximum6.
- Coût : 100 images d'origine incluses par mois sur Pro et Team, puis 5 $ par tranche de 1 0006.
- Les URL signées acceptent aussi l'option
transform; les paramètres sont alors figés dans le jeton6.
Comment partager un fichier privé ?
Deux voies pour un bucket privé : télécharger avec le JWT de l'utilisateur (les policies décident), ou générer une URL signée valable un temps limité3.
// URL valable une heure (durée en secondes)
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl('factures/facture-123.pdf', 3600)
// Téléchargement authentifié puis enregistrement côté navigateur
const { data: blob } = await supabase.storage.from('documents').download('factures/facture-123.pdf')
if (blob) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'facture-123.pdf'
a.click()
URL.revokeObjectURL(url)
}Comment lister, déplacer et supprimer ?
const { data: files } = await supabase.storage
.from('avatars')
.list(userId, { limit: 100, sortBy: { column: 'created_at', order: 'desc' } })
await supabase.storage.from('photos').move('ancien-nom.jpg', 'nouveau-nom.jpg')
await supabase.storage.from('photos').remove(['photo1.jpg', 'photo2.jpg'])Supprimez toujours par l'API Storage (remove, 1 000 objets maximum par appel), jamais par un delete SQL sur storage.objects : le fichier resterait dans le bucket, orphelin. Pour vider un bucket de plus de 200 000 objets, Supabase recommande l'AWS CLI via le protocole S37. Enfin, les sauvegardes de la base ne contiennent pas les fichiers Storage, seulement leurs métadonnées8.
Pour traiter les fichiers côté serveur après l'envoi (redimensionnement, extraction de texte), voir les fonctions serverless Edge Functions, ou notre service de développement sur mesure pour un projet complet.
Sources
- Storage Access Control (documentation Supabase)
- S3 Compatibility (documentation Supabase)
- Storage Buckets (documentation Supabase)
- Creating Buckets (documentation Supabase)
- Storage Limits (documentation Supabase)
- Storage Image Transformations (documentation Supabase)
- Delete Objects (documentation Supabase)
- Database Backups (documentation Supabase)






Commentaires
Chaque commentaire est relu avant publication, en général sous 24 h ouvrées. Les liens promotionnels ne sont pas publiés.
Aucun commentaire pour l’instant. Une question sur l’article ? Posez-la ci-dessous.