Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Organize Folders in Supabase Storage

Supabase Storage uses path prefixes as virtual folders — there are no real folder objects. Organize files by including folder-like segments in the file path when uploading, such as 'user_id/photos/image.png'. Use the user's auth ID as the top-level folder for per-user isolation and combine this with RLS policies on storage.objects that check the first path segment against auth.uid().

What you'll learn

  • How Supabase Storage virtual folders work using path prefixes
  • How to implement user-scoped folder patterns for file isolation
  • How to create folder structures when uploading files
  • How to write RLS policies that use folder paths for access control
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read10-15 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

Supabase Storage uses path prefixes as virtual folders — there are no real folder objects. Organize files by including folder-like segments in the file path when uploading, such as 'user_id/photos/image.png'. Use the user's auth ID as the top-level folder for per-user isolation and combine this with RLS policies on storage.objects that check the first path segment against auth.uid().

Organizing Files with Virtual Folders in Supabase Storage

Unlike traditional file systems, Supabase Storage does not have real directories. Instead, forward slashes in file paths create virtual folder structures. This tutorial shows you how to design effective folder hierarchies, implement user-scoped storage patterns, and write RLS policies that leverage folder paths for security.

Prerequisites

  • A Supabase project with at least one storage bucket
  • The Supabase JS client installed (@supabase/supabase-js v2+)
  • Basic understanding of Row Level Security in Supabase
  • An authentication setup with signed-in users

Step-by-step guide

1

Understand virtual folders in Supabase Storage

Supabase Storage is backed by S3-compatible object storage where every file is a flat object with a path-like name. The forward slashes in file names create the appearance of folders. When you upload a file to 'invoices/2024/january/report.pdf', Supabase stores it as a single object with that full path as its name. The Dashboard and list() method present these as nested folders for convenience, but there are no actual folder objects to create or manage. This means you cannot create empty folders — a folder only appears when it contains at least one file.

Expected result: You understand that folders in Supabase Storage are virtual path prefixes, not real directory objects.

2

Design a user-scoped folder structure

The most common and secure pattern is to use the authenticated user's ID as the top-level folder. This creates natural isolation between users and maps directly to RLS policies. Below the user ID folder, organize by content type or purpose. For example: '{user_id}/avatars/', '{user_id}/documents/', '{user_id}/photos/'. This structure makes it easy to list all of a user's files, grant access only to their own data, and clean up when a user is deleted.

typescript
1// Upload to user-scoped folder structure
2const { data: { user } } = await supabase.auth.getUser()
3const userId = user?.id
4
5// User's avatar
6await supabase.storage
7 .from('user-files')
8 .upload(`${userId}/avatars/profile.jpg`, avatarFile)
9
10// User's document in a date-based subfolder
11const today = new Date().toISOString().split('T')[0]
12await supabase.storage
13 .from('user-files')
14 .upload(`${userId}/documents/${today}/report.pdf`, docFile)

Expected result: Files are organized under the user's ID with category-based subfolders.

3

Write RLS policies that enforce folder-based access

The storage.foldername(name) function extracts folder path segments from a file's full path. Use this in RLS policies to ensure users can only access files in their own folder. The function returns an array of folder segments — the first element [1] is the top-level folder. By checking that the first folder segment equals the user's ID, you restrict all operations to the user's own files.

typescript
1-- Allow users to upload only to their own folder
2create policy "User upload to own folder"
3on storage.objects for insert
4to authenticated
5with check (
6 bucket_id = 'user-files'
7 and auth.uid()::text = (storage.foldername(name))[1]
8);
9
10-- Allow users to read only their own files
11create policy "User read own folder"
12on storage.objects for select
13to authenticated
14using (
15 bucket_id = 'user-files'
16 and auth.uid()::text = (storage.foldername(name))[1]
17);
18
19-- Allow users to delete only their own files
20create policy "User delete own folder"
21on storage.objects for delete
22to authenticated
23using (
24 bucket_id = 'user-files'
25 and auth.uid()::text = (storage.foldername(name))[1]
26);

Expected result: RLS policies enforce that each user can only access files within their own user ID folder.

4

Create shared folders with team-based access

For applications where teams share files, use a team or organization ID as the folder prefix instead of the user ID. Write RLS policies that check team membership by joining against a teams or team_members table. This allows multiple users to access the same folder while still preventing access from users outside the team.

typescript
1-- Team-based folder access
2create policy "Team members can read team files"
3on storage.objects for select
4to authenticated
5using (
6 bucket_id = 'team-files'
7 and (storage.foldername(name))[1] in (
8 select team_id::text
9 from team_members
10 where user_id = (select auth.uid())
11 )
12);
13
14-- Team members can upload to their team folder
15create policy "Team members can upload to team folder"
16on storage.objects for insert
17to authenticated
18with check (
19 bucket_id = 'team-files'
20 and (storage.foldername(name))[1] in (
21 select team_id::text
22 from team_members
23 where user_id = (select auth.uid())
24 )
25);

Expected result: Team members can read and upload files in their team's shared folder.

5

List and navigate folder contents

Use the list() method with a folder path to browse the virtual folder structure. Pass the folder path as the first argument. The returned items include both files and subfolders at that level. Subfolders have a null id field. Build a recursive navigator by calling list() on each subfolder as the user clicks into it.

typescript
1// List root folders for the current user
2const { data: { user } } = await supabase.auth.getUser()
3
4const { data: rootItems } = await supabase.storage
5 .from('user-files')
6 .list(user?.id)
7
8// Separate folders and files
9const folders = rootItems?.filter((item) => item.id === null) || []
10const files = rootItems?.filter((item) => item.id !== null) || []
11
12console.log('Folders:', folders.map((f) => f.name))
13console.log('Files:', files.map((f) => f.name))
14
15// Navigate into a subfolder
16const { data: subItems } = await supabase.storage
17 .from('user-files')
18 .list(`${user?.id}/documents`)

Expected result: File and subfolder listings are returned for each folder path, enabling folder navigation.

Complete working example

storage-folder-manager.ts
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Get the authenticated user's ID for folder scoping
9async function getUserId(): Promise<string> {
10 const { data: { user }, error } = await supabase.auth.getUser()
11 if (error || !user) throw new Error('User not authenticated')
12 return user.id
13}
14
15// Upload a file to the user's scoped folder
16async function uploadToUserFolder(
17 category: string,
18 fileName: string,
19 file: File
20) {
21 const userId = await getUserId()
22 const path = `${userId}/${category}/${fileName}`
23
24 const { data, error } = await supabase.storage
25 .from('user-files')
26 .upload(path, file, { upsert: false })
27
28 if (error) throw new Error(`Upload failed: ${error.message}`)
29 return data.path
30}
31
32// List contents of a user's folder
33async function listUserFolder(subfolder?: string) {
34 const userId = await getUserId()
35 const path = subfolder ? `${userId}/${subfolder}` : userId
36
37 const { data, error } = await supabase.storage
38 .from('user-files')
39 .list(path, {
40 limit: 100,
41 sortBy: { column: 'name', order: 'asc' },
42 })
43
44 if (error) throw new Error(`List failed: ${error.message}`)
45
46 return {
47 folders: data.filter((item) => item.id === null),
48 files: data.filter((item) => item.id !== null),
49 }
50}
51
52// Move a file by copying then deleting (no native move)
53async function moveFile(oldPath: string, newPath: string) {
54 const userId = await getUserId()
55 const fullOldPath = `${userId}/${oldPath}`
56 const fullNewPath = `${userId}/${newPath}`
57
58 const { data: file } = await supabase.storage
59 .from('user-files')
60 .download(fullOldPath)
61
62 if (!file) throw new Error('Source file not found')
63
64 await supabase.storage.from('user-files').upload(fullNewPath, file)
65 await supabase.storage.from('user-files').remove([fullOldPath])
66}
67
68// === Usage ===
69const { folders, files } = await listUserFolder('documents')
70console.log('Subfolders:', folders.map((f) => f.name))
71console.log('Files:', files.map((f) => f.name))

Common mistakes when organizing Folders in Supabase Storage

Why it's a problem: Trying to create empty folders before uploading files

How to avoid: Supabase Storage does not support empty folders. Folders are virtual and only appear when they contain at least one file. Just upload files with the desired path and folders appear automatically.

Why it's a problem: Hardcoding user IDs in file paths instead of using auth.uid()

How to avoid: Always derive the user ID from supabase.auth.getUser() at runtime. Hardcoded IDs create security gaps and break when used by different users.

Why it's a problem: Using backslashes instead of forward slashes in folder paths

How to avoid: Supabase Storage uses forward slashes (/) as path separators, matching S3 conventions. Backslashes are treated as part of the file name and will not create folder structure.

Best practices

  • Use the authenticated user's UUID as the top-level folder for natural per-user isolation
  • Organize subfolders by content category (avatars, documents, photos) for logical grouping
  • Write RLS policies that check storage.foldername(name)[1] against auth.uid() for security
  • Keep folder nesting shallow — two or three levels deep is usually sufficient
  • Use consistent naming conventions for folders (lowercase, hyphens or underscores)
  • Never trust client-supplied folder paths without validating them against the authenticated user's ID
  • Add date-based subfolders for time-series content to make cleanup and archival easier

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I want to organize files in Supabase Storage with user-specific folders. Each user should have their own top-level folder using their auth ID, with subfolders for 'avatars', 'documents', and 'exports'. Show me the upload code, list code, and the RLS policies needed.

Supabase Prompt

Set up a Supabase Storage bucket called 'user-files' with a user-scoped folder structure. Write RLS policies using storage.foldername() to restrict each user to their own folder. Create helper functions for uploading, listing, and navigating the folder hierarchy.

Frequently asked questions

Can I create an empty folder in Supabase Storage?

No, Supabase Storage uses virtual folders based on file path prefixes. Folders only appear when they contain at least one file. You cannot create a standalone empty folder.

How do I rename a folder?

There is no native rename operation for folders. You need to copy all files from the old path to the new path and then delete the originals. For large folders, do this in batches to avoid timeouts.

What characters are allowed in folder names?

Folder names (path segments) can contain letters, numbers, hyphens, underscores, and periods. Avoid spaces and special characters as they need URL encoding. Stick to lowercase alphanumeric characters with hyphens for consistency.

Is there a limit to folder nesting depth?

There is no hard limit on nesting depth, but the total file path (including all folder segments) must stay within S3's 1024-character key limit. In practice, keep nesting to three or four levels for simplicity.

How does the storage.foldername function work?

storage.foldername(name) takes the file's full path and returns an array of folder segments. For 'user123/documents/report.pdf', it returns {'user123', 'documents'}. The [1] index gets the first folder, [2] the second, and so on.

Can I move files between folders?

Supabase Storage does not have a native move operation. To move a file, download it, upload it to the new path, and then delete the original. The copy() method can also be used with the storage API.

Can RapidDev help design a folder structure for my Supabase project?

Yes, RapidDev can design and implement an optimized folder structure for your Supabase Storage, including user-scoped paths, team sharing patterns, and the necessary RLS policies for secure access.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.