ignitionstack.pro v1.0 is out! Read the announcement →
Skip to Content

Supabase Storage

This guide explains how to configure and use Supabase Storage for uploading images and files in the project.

Initial Configuration

1. Create a Bucket in Supabase

  1. Access the Supabase Dashboard 
  2. Navigate to Storage in the side menu
  3. Click New bucket
  4. Configure:
    • Name: uploads (or another desired name)
    • Public bucket: Enable this option for publicly accessible files
    • File size limit: Configure as needed (e.g., 5MB for images)
    • Allowed MIME types: image/jpeg, image/png, image/gif, image/webp (optional)

2. Configure Access Policies (RLS)

After creating the bucket, configure security policies:

-- Allow public read access to all files CREATE POLICY "Allow public read access" ON storage.objects FOR SELECT USING (bucket_id = 'uploads'); -- Allow upload only for authenticated users CREATE POLICY "Allow authenticated uploads" ON storage.objects FOR INSERT WITH CHECK ( bucket_id = 'uploads' AND auth.role() = 'authenticated' ); -- Allow users to delete their own files (optional) CREATE POLICY "Allow users to delete own files" ON storage.objects FOR DELETE USING ( bucket_id = 'uploads' AND auth.uid()::text = (storage.foldername(name))[1] );

Note: If you use createAdminClient() (Service Role Key), these policies are bypassed.

Folder Structure in Bucket

The project organizes files in the uploads bucket like this:

uploads/ ├── blog/ # Blog post images │ └── {uuid}.jpg ├── products/ # Product images │ └── {uuid}.png ├── avatars/ # User avatars │ └── {user-id}/{uuid}.jpg └── projects/ # Project images └── {uuid}.webp

Using Storage in Code

Upload Image (Server Action)

The project already has a configured action in src/app/actions/blog/upload-image.ts:

import { createAdminClient } from "@/app/lib/supabase/server"; import { v4 as uuidv4 } from "uuid"; const BUCKET_NAME = "uploads"; export async function uploadImage( file: Buffer, filename: string ): Promise<{ success: boolean; url?: string; error?: string }> { const supabase = createAdminClient(); const fileExtension = filename.split(".").pop()?.toLowerCase() || "jpg"; const uniqueFilename = `blog/${uuidv4()}.${fileExtension}`; const { data, error } = await supabase.storage .from(BUCKET_NAME) .upload(uniqueFilename, file, { contentType: `image/${fileExtension}`, cacheControl: "3600", upsert: false, }); if (error) { return { success: false, error: error.message }; } const { data: { publicUrl } } = supabase.storage .from(BUCKET_NAME) .getPublicUrl(data.path); return { success: true, url: publicUrl }; }

Available Helpers

The project includes helpers in src/app/lib/supabase/helpers.ts:

// Upload file (returns public URL) const url = await uploadFile(supabase, 'uploads', file, 'path/file.jpg'); // Delete file await deleteFile(supabase, 'uploads', 'path/file.jpg'); // Get public URL const publicUrl = getPublicUrl(supabase, 'uploads', 'path/file.jpg'); // Create signed URL (with expiration) const signedUrl = await getSignedUrl(supabase, 'uploads', 'path/file.jpg', 3600);

Usage in Client Components

"use client"; import { createClient } from "@/app/lib/supabase/client"; async function uploadFromClient(file: File) { const supabase = createClient(); const { data, error } = await supabase.storage .from("uploads") .upload(`user-uploads/${file.name}`, file); if (error) throw error; const { data: { publicUrl } } = supabase.storage .from("uploads") .getPublicUrl(data.path); return publicUrl; }

ImageUploader Component

The project includes a reusable component at src/app/components/admin/image-uploader.tsx:

import ImageUploader from "@/app/components/admin/image-uploader"; // In your form <ImageUploader value={imageUrl} onChange={(url) => setImageUrl(url)} label="Cover Image" />

The component:

Best Practices

1. Use UUIDs for File Names

const uniqueFilename = `${uuidv4()}.${extension}`;

Avoids conflicts and information exposure.

2. Organize in Folders

const path = `${folder}/${userId}/${filename}`;

Facilitates management and access policies.

3. Set Correct Content-Type

await supabase.storage.from("bucket").upload(path, file, { contentType: "image/jpeg", });

Ensures the browser interprets correctly.

4. Configure Cache Control

await supabase.storage.from("bucket").upload(path, file, { cacheControl: "3600", // 1 hour in seconds });

Improves performance and reduces costs.

5. Validate Uploads on Server

// Validate type if (!file.type.startsWith("image/")) { throw new Error("Only images allowed"); } // Validate size if (file.size > 5 * 1024 * 1024) { throw new Error("File too large (max 5MB)"); }

Troubleshooting

Error: “Bucket not found”

Error: “new row violates row-level security policy”

Error: “The resource already exists”

await supabase.storage.from("bucket").upload(path, file, { upsert: true, });

Image doesn’t load