# How to Organize Folders in Supabase Storage

- Tool: Supabase
- Difficulty: Intermediate
- Time required: 10-15 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

## 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.

## Before you start

- 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.

```
// Upload to user-scoped folder structure
const { data: { user } } = await supabase.auth.getUser()
const userId = user?.id

// User's avatar
await supabase.storage
  .from('user-files')
  .upload(`${userId}/avatars/profile.jpg`, avatarFile)

// User's document in a date-based subfolder
const today = new Date().toISOString().split('T')[0]
await supabase.storage
  .from('user-files')
  .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.

```
-- Allow users to upload only to their own folder
create policy "User upload to own folder"
on storage.objects for insert
to authenticated
with check (
  bucket_id = 'user-files'
  and auth.uid()::text = (storage.foldername(name))[1]
);

-- Allow users to read only their own files
create policy "User read own folder"
on storage.objects for select
to authenticated
using (
  bucket_id = 'user-files'
  and auth.uid()::text = (storage.foldername(name))[1]
);

-- Allow users to delete only their own files
create policy "User delete own folder"
on storage.objects for delete
to authenticated
using (
  bucket_id = 'user-files'
  and auth.uid()::text = (storage.foldername(name))[1]
);
```

**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.

```
-- Team-based folder access
create policy "Team members can read team files"
on storage.objects for select
to authenticated
using (
  bucket_id = 'team-files'
  and (storage.foldername(name))[1] in (
    select team_id::text
    from team_members
    where user_id = (select auth.uid())
  )
);

-- Team members can upload to their team folder
create policy "Team members can upload to team folder"
on storage.objects for insert
to authenticated
with check (
  bucket_id = 'team-files'
  and (storage.foldername(name))[1] in (
    select team_id::text
    from team_members
    where user_id = (select auth.uid())
  )
);
```

**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.

```
// List root folders for the current user
const { data: { user } } = await supabase.auth.getUser()

const { data: rootItems } = await supabase.storage
  .from('user-files')
  .list(user?.id)

// Separate folders and files
const folders = rootItems?.filter((item) => item.id === null) || []
const files = rootItems?.filter((item) => item.id !== null) || []

console.log('Folders:', folders.map((f) => f.name))
console.log('Files:', files.map((f) => f.name))

// Navigate into a subfolder
const { data: subItems } = await supabase.storage
  .from('user-files')
  .list(`${user?.id}/documents`)
```

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

## Complete code example

File: `storage-folder-manager.ts`

```typescript
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Get the authenticated user's ID for folder scoping
async function getUserId(): Promise<string> {
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) throw new Error('User not authenticated')
  return user.id
}

// Upload a file to the user's scoped folder
async function uploadToUserFolder(
  category: string,
  fileName: string,
  file: File
) {
  const userId = await getUserId()
  const path = `${userId}/${category}/${fileName}`

  const { data, error } = await supabase.storage
    .from('user-files')
    .upload(path, file, { upsert: false })

  if (error) throw new Error(`Upload failed: ${error.message}`)
  return data.path
}

// List contents of a user's folder
async function listUserFolder(subfolder?: string) {
  const userId = await getUserId()
  const path = subfolder ? `${userId}/${subfolder}` : userId

  const { data, error } = await supabase.storage
    .from('user-files')
    .list(path, {
      limit: 100,
      sortBy: { column: 'name', order: 'asc' },
    })

  if (error) throw new Error(`List failed: ${error.message}`)

  return {
    folders: data.filter((item) => item.id === null),
    files: data.filter((item) => item.id !== null),
  }
}

// Move a file by copying then deleting (no native move)
async function moveFile(oldPath: string, newPath: string) {
  const userId = await getUserId()
  const fullOldPath = `${userId}/${oldPath}`
  const fullNewPath = `${userId}/${newPath}`

  const { data: file } = await supabase.storage
    .from('user-files')
    .download(fullOldPath)

  if (!file) throw new Error('Source file not found')

  await supabase.storage.from('user-files').upload(fullNewPath, file)
  await supabase.storage.from('user-files').remove([fullOldPath])
}

// === Usage ===
const { folders, files } = await listUserFolder('documents')
console.log('Subfolders:', folders.map((f) => f.name))
console.log('Files:', files.map((f) => f.name))
```

## Common mistakes

- **Trying to create empty folders before uploading files** — undefined Fix: 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.
- **Hardcoding user IDs in file paths instead of using auth.uid()** — undefined Fix: Always derive the user ID from supabase.auth.getUser() at runtime. Hardcoded IDs create security gaps and break when used by different users.
- **Using backslashes instead of forward slashes in folder paths** — undefined Fix: 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

## 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.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-organize-folders-in-supabase-storage
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-organize-folders-in-supabase-storage
