This guide explains how to configure and use Supabase Storage for uploading images and files in the project.
uploads (or another desired name)image/jpeg, image/png, image/gif, image/webp (optional)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.
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}.webpThe 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 };
}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);"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;
}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:
const uniqueFilename = `${uuidv4()}.${extension}`;Avoids conflicts and information exposure.
const path = `${folder}/${userId}/${filename}`;Facilitates management and access policies.
await supabase.storage.from("bucket").upload(path, file, {
contentType: "image/jpeg",
});Ensures the browser interprets correctly.
await supabase.storage.from("bucket").upload(path, file, {
cacheControl: "3600", // 1 hour in seconds
});Improves performance and reduces costs.
// 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)");
}createAdminClient() for bypassing RLS (server-side only)upsert: true to overwrite:await supabase.storage.from("bucket").upload(path, file, {
upsert: true,
});