Stockage de fichiers
Téléversement et diffusion de fichiers utilisateur (images, PDF, avatars) avec Supabase Storage et Row Level Security.
Stockage de fichiers
ScaleRocket utilise Supabase Storage pour tous les fichiers téléversés par les utilisateurs. Il est construit sur S3, intégré avec votre authentification Supabase existante et le RLS, et ne nécessite aucun service supplémentaire à configurer.
Où stocker quoi
| Type de fichier | Où | Pourquoi |
|---|---|---|
| Logo, favicon, illustrations du site | apps/web/public/ | Ressources statiques, déployées avec votre code |
| Téléversements utilisateur (images, PDF) | Supabase Storage (bucket public ou privé) | Dynamiques, par utilisateur, avec contrôle d'accès |
| Avatars, photos de profil | Supabase Storage (bucket public) | Doivent être visibles par les autres |
| Documents sensibles | Supabase Storage (bucket privé) | Seul le propriétaire doit y accéder |
| Exports générés, rapports | Supabase Storage (bucket privé) | Créés par l'application, téléchargés par l'utilisateur |
Créer un bucket de stockage
Via le tableau de bord Supabase
- Allez dans Storage dans votre tableau de bord Supabase
- Cliquez sur New bucket
- Entrez un nom (par ex.
uploads,avatars,documents) - Choisissez Public ou Private :
- Public — N'importe qui avec l'URL peut voir le fichier (idéal pour les avatars, images de produits)
- Private — Nécessite une authentification pour accéder (idéal pour les documents utilisateur, exports)
- Cliquez sur Create bucket
Via migration (recommandé)
Ajoutez une migration pour que le bucket soit créé automatiquement lorsque quelqu'un installe votre boilerplate :
-- supabase/migrations/00016_storage_buckets.sql
-- Public bucket for user-facing images (avatars, uploads displayed on the site)
INSERT INTO storage.buckets (id, name, public)
VALUES ('uploads', 'uploads', true)
ON CONFLICT (id) DO NOTHING;
-- Private bucket for sensitive documents
INSERT INTO storage.buckets (id, name, public)
VALUES ('documents', 'documents', false)
ON CONFLICT (id) DO NOTHING;Politiques RLS pour le stockage
Tout comme les tables de base de données, les buckets de stockage utilisent le Row Level Security pour contrôler qui peut lire, téléverser et supprimer des fichiers.
Bucket public (par ex. uploads)
Les utilisateurs peuvent téléverser dans leur propre dossier et n'importe qui peut voir les fichiers :
-- Anyone can view files in the public bucket
CREATE POLICY "Public read access"
ON storage.objects FOR SELECT
USING (bucket_id = 'uploads');
-- Authenticated users can upload to their own folder
CREATE POLICY "Users can upload own files"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'uploads'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Users can delete their own files
CREATE POLICY "Users can delete own files"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'uploads'
AND auth.uid()::text = (storage.foldername(name))[1]
);Le modèle clé est auth.uid()::text = (storage.foldername(name))[1] — cela signifie que les fichiers de chaque utilisateur sont stockés dans un dossier nommé d'après leur identifiant utilisateur. L'utilisateur abc123 téléverse dans uploads/abc123/photo.jpg.
Bucket privé (par ex. documents)
Seul le propriétaire du fichier peut lire et gérer :
-- Only the owner can read their own files
CREATE POLICY "Users can read own documents"
ON storage.objects FOR SELECT
TO authenticated
USING (
bucket_id = 'documents'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Users can upload to their own folder
CREATE POLICY "Users can upload own documents"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'documents'
AND auth.uid()::text = (storage.foldername(name))[1]
);
-- Users can delete their own documents
CREATE POLICY "Users can delete own documents"
ON storage.objects FOR DELETE
TO authenticated
USING (
bucket_id = 'documents'
AND auth.uid()::text = (storage.foldername(name))[1]
);Téléverser des fichiers depuis votre application
Téléversement basique
// apps/app/src/lib/storage.ts
import { supabase } from "./supabase/client";
export async function uploadFile(
bucket: string,
file: File,
path?: string
) {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Not authenticated");
// Store in user's folder: {user_id}/{filename}
const filePath = path || `${user.id}/${Date.now()}-${file.name}`;
const { data, error } = await supabase.storage
.from(bucket)
.upload(filePath, file, {
cacheControl: "3600",
upsert: false,
});
if (error) throw error;
// Get the public URL (for public buckets)
const { data: urlData } = supabase.storage
.from(bucket)
.getPublicUrl(data.path);
return {
path: data.path,
url: urlData.publicUrl,
};
}Composant de téléversement (React)
// apps/app/src/components/FileUpload.tsx
import { useState } from "react";
import { Button } from "@saas/ui";
import { uploadFile } from "@/lib/storage";
export function FileUpload({
bucket = "uploads",
onUpload,
}: {
bucket?: string;
onUpload: (url: string) => void;
}) {
const [uploading, setUploading] = useState(false);
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const { url } = await uploadFile(bucket, file);
onUpload(url);
} catch (err) {
console.error("Upload failed:", err);
} finally {
setUploading(false);
}
};
return (
<div>
<input
type="file"
onChange={handleChange}
disabled={uploading}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload">
<Button asChild isLoading={uploading}>
<span>{uploading ? "Uploading..." : "Upload file"}</span>
</Button>
</label>
</div>
);
}Télécharger et afficher des fichiers
Obtenir l'URL publique
Pour les buckets publics, l'URL est prévisible :
const { data } = supabase.storage
.from("uploads")
.getPublicUrl("user-id/photo.jpg");
// data.publicUrl = "https://xxx.supabase.co/storage/v1/object/public/uploads/user-id/photo.jpg"Obtenir une URL signée (buckets privés)
Pour les buckets privés, générez une URL signée temporaire :
const { data, error } = await supabase.storage
.from("documents")
.createSignedUrl("user-id/contract.pdf", 3600); // expires in 1 hour
// data.signedUrl = "https://xxx.supabase.co/storage/v1/object/sign/documents/..."Afficher une image
<img
src={supabase.storage.from("uploads").getPublicUrl("user-id/avatar.jpg").data.publicUrl}
alt="User avatar"
className="h-16 w-16 rounded-full object-cover"
/>Supprimer des fichiers
const { error } = await supabase.storage
.from("uploads")
.remove(["user-id/old-photo.jpg"]);Pour supprimer plusieurs fichiers :
const { error } = await supabase.storage
.from("uploads")
.remove([
"user-id/photo1.jpg",
"user-id/photo2.jpg",
"user-id/photo3.jpg",
]);Lister les fichiers
const { data, error } = await supabase.storage
.from("uploads")
.list("user-id/", {
limit: 100,
offset: 0,
sortBy: { column: "created_at", order: "desc" },
});
// data = [{ name: "photo.jpg", id: "...", created_at: "...", ... }, ...]Validation des fichiers
Validez toujours les fichiers avant le téléversement :
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
function validateFile(file: File): string | null {
if (file.size > MAX_FILE_SIZE) {
return "File is too large. Maximum size is 10MB.";
}
if (!ALLOWED_TYPES.includes(file.type)) {
return "File type not allowed. Use JPEG, PNG, WebP, or PDF.";
}
return null; // valid
}Limites de stockage
| Plan Supabase | Stockage | Bande passante |
|---|---|---|
| Free | 1 Go | 2 Go/mois |
| Pro (25$/mois) | 100 Go | 250 Go/mois |
| Team (599$/mois) | Illimité | Illimité |
Pour la plupart des applications SaaS, le plan Pro est largement suffisant. Si vous avez besoin de plus, envisagez de décharger les gros fichiers vers un CDN dédié (Cloudflare R2, AWS S3).
Prochaines étapes
- Configurez votre base de données pour stocker les métadonnées de fichiers avec vos données
- Mettez en place le système de crédits si le traitement de fichiers coûte des crédits
- Déployez en production — Les buckets de stockage sont créés sur votre projet Supabase de production via les migrations
Fini ? Marquez cette page comme terminée.