ScaleRocket/Web

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 fichierPourquoi
Logo, favicon, illustrations du siteapps/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 profilSupabase Storage (bucket public)Doivent être visibles par les autres
Documents sensiblesSupabase Storage (bucket privé)Seul le propriétaire doit y accéder
Exports générés, rapportsSupabase 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

  1. Allez dans Storage dans votre tableau de bord Supabase
  2. Cliquez sur New bucket
  3. Entrez un nom (par ex. uploads, avatars, documents)
  4. 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)
  5. 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 SupabaseStockageBande passante
Free1 Go2 Go/mois
Pro (25$/mois)100 Go250 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

Fini ? Marquez cette page comme terminée.

On this page