Supabase Storage Upload: What You'll Build
Supabase Storage lets you store files (avatars, documents, images, videos) next to your Postgres database, with the same auth and permissions. Uploading takes a few lines of JavaScript, but permissions trip up almost everyone the first time.
This guide covers the full path: create a bucket, set the right policies, upload from a Next.js or React app, get a URL to display the file, and fix the common errors.
๐ฏ Quick Answer (30-Second Read)
- Create a bucket in the dashboard (Storage โ New bucket): public for open assets, private for user files.
- Add RLS policies on
storage.objects, or uploads fail with "new row violates row-level security policy". - Upload:
supabase.storage.from('bucket').upload(path, file). - Display:
getPublicUrl()for public buckets,createSignedUrl()(time-limited) for private ones. - Organise by user: store files under
userId/filenameso policies can restrict each user to their own folder.
How Supabase Storage Works
Storage has two parts: the files live in S3-compatible object storage, and each file's metadata (bucket, path, owner) is a row in your Postgres database, in storage.objects. That's why Postgres RLS policies control who can upload and read.
Step 1: Create a Bucket
In the Supabase dashboard: Storage โ New bucket.
Anyone with the URL can view files. Good for blog images, logos and public avatars. Uploads still need a policy.
Files need permission to read. Good for invoices, documents and anything personal. Share via signed URLs.
You can also set limits such as max file size and allowed MIME types (for example image/png, image/jpeg) on the bucket.
Step 2: Add Storage Policies
Storage keeps file records in the storage.objects table, which has Row Level Security enabled. Without a policy, uploads are rejected.
Let each logged-in user upload, read and replace files only in their own folder (avatars/<user-id>/...):
create policy "Users upload to own folder"
on storage.objects for insert to authenticated
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "Users read own files"
on storage.objects for select to authenticated
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.uid())::text
);
create policy "Users update own files"
on storage.objects for update to authenticated
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = (select auth.uid())::text
);storage.foldername(name) splits the path into folders, so [1] is the first folder: the user's ID.
Step 3: Upload a File From Your App
"use client";
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export function AvatarUpload() {
async function onChange(e: React.ChangeEvent) {
const file = e.target.files?.[0];
if (!file) return;
const { data: { user } } = await supabase.auth.getUser();
if (!user) return alert("Please log in first");
const path = `${user.id}/${Date.now()}-${file.name}`;
const { data, error } = await supabase.storage
.from("avatars")
.upload(path, file, { cacheControl: "3600", upsert: false });
if (error) return alert(error.message);
console.log("Uploaded to:", data.path);
}
return ;
} upsert: true, which also needs an UPDATE policy.
Step 4: Get a URL to Display the File
Public bucket: a permanent URL.
const { data } = supabase.storage.from("avatars").getPublicUrl(path);
console.log(data.publicUrl);Private bucket: a signed URL that expires.
const { data, error } = await supabase.storage
.from("documents")
.createSignedUrl(path, 60 * 60); // valid for 1 hourSave the path (not the URL) in your database, then generate URLs when you need them. That way you can switch between public and signed URLs later.
Step 5: Replace and Delete Files
// Replace (needs an UPDATE policy)
await supabase.storage.from("avatars").upload(path, newFile, { upsert: true });
// Delete (needs a DELETE policy)
await supabase.storage.from("avatars").remove([path]);Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
new row violates row-level security policy |
No INSERT policy on storage.objects (or user not logged in) |
Add the policy in Step 2. See the full fix guide |
The resource already exists |
Path already used | Use a unique file name or upsert: true (+ UPDATE policy) |
| File too large / invalid MIME type | Bucket limits | Raise the limit in bucket settings or validate before upload |
| Image URL returns 400/404 | Wrong path, or private bucket used with getPublicUrl |
Check the stored path; use signed URLs for private buckets |
Uploading Large Files
For large files (like videos), Supabase supports resumable uploads using the TUS protocol, so an interrupted upload can continue instead of restarting. Use it for anything big or for users on unreliable connections.
Frequently Asked Questions
Do public buckets still need policies to upload?
Yes. "Public" only means anyone can read files with the URL. Uploading, updating and deleting are still controlled by policies on storage.objects, so add an INSERT policy for the users who should be able to upload.
Should I upload from the browser or the server?
Browser uploads with the user's session are simplest and scale well, because files go straight to Supabase and policies protect each user's folder. Upload from the server when you need to process files first or use privileged access, and keep the service-role key on the server only.
How do I show a private image in my app?
Store the file path in your database, then call createSignedUrl(path, seconds) when rendering. The signed URL works for the time you set, after which it expires, so private files can't be shared permanently by copying a link.
Conclusion
Uploading to Supabase Storage takes three things: a bucket, policies on storage.objects, and one upload() call. Organise files by user ID, store paths in your database, use public URLs for open assets and signed URLs for private files, and most errors disappear.
Sources
- Supabase docs: Storage uploads
- Supabase docs: Storage access control
- Supabase docs: Resumable uploads
- supabase-js reference: storage
Last verified: 14 October 2026.
Related reads: Fix Supabase RLS Policy Error ยท How to Create a SaaS With Next.js and Supabase ยท How to Use Environment Variables in Next.js
I write about building products with Next.js, Supabase and AI. Follow on LinkedIn or X, or invite me to speak.