# Folder operations and hierarchical policies in Storage

> Storage folders are path prefixes, not real directories. How to list, move and delete a whole folder with supabase-js, and how to write policies based on folder names.

Source: https://supahost.app/kb/storage-folder-operations · Updated 2026-11-11

Storage has no real directories. An object's `name` is its full path, like `team_42/reports/q3.pdf`, and "folders" are just the parts between slashes. That's why there's no single call to delete or move a folder, and why policies can match on path segments.

## What you see

- `remove(['team_42/reports'])` succeeds but nothing is deleted.
- `list('team_42')` shows `reports` as an entry with `id: null`, not its files.
- A folder created in Studio contains a file called `.emptyFolderPlaceholder`.

## Why it happens

`remove()` and `move()` work on exact object names. `team_42/reports` isn't an object, so there's nothing to remove. `list()` returns one level at a time: files at that level, plus one entry per sub-prefix (those have `id: null`). Studio creates an empty placeholder object so that an empty folder can exist at all.

Storage provides SQL helpers that split a name for use in policies:

| Function | For `team_42/reports/q3.pdf` returns |
|---|---|
| `storage.foldername(name)` | `{team_42,reports}` |
| `storage.filename(name)` | `q3.pdf` |
| `storage.extension(name)` | `pdf` |

## How to fix it

1. **Delete a folder** by listing everything under it, then removing in batches:

   ```js
   async function listAll(bucket, prefix) {
     const files = []
     const { data, error } = await supabase.storage.from(bucket).list(prefix, { limit: 1000 })
     if (error) throw error
     for (const entry of data) {
       const path = `${prefix}/${entry.name}`
       if (entry.id === null) files.push(...await listAll(bucket, path)) // sub-folder
       else files.push(path)
     }
     return files
   }

   const paths = await listAll('team-files', 'team_42/reports')
   for (let i = 0; i < paths.length; i += 100) {
     const { error } = await supabase.storage.from('team-files').remove(paths.slice(i, i + 100))
     if (error) throw error
   }
   ```

   A folder with more than 1000 entries at one level needs the `offset` option to page through.

2. **Move or rename a folder** by moving each file:

   ```js
   for (const path of await listAll('team-files', 'team_42/drafts')) {
     await supabase.storage.from('team-files').move(path, path.replace('team_42/drafts/', 'team_42/archive/'))
   }
   ```

3. **Write a team policy on the first folder.** With paths like `<team_id>/...` and a membership table:

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

   Copy it for `insert` (as `with check`), `update` and `delete`, and index `team_members (user_id, team_id)` so the check stays fast.

4. **Restrict a sub-folder** by checking deeper segments. Only team admins may write under `<team_id>/billing/`:

   ```sql
   create policy "Admins upload billing files"
   on storage.objects for insert to authenticated
   with check (
     bucket_id = 'team-files'
     and (storage.foldername(name))[2] = 'billing'
     and exists (
       select 1 from public.team_members m
       where m.team_id::text = (storage.foldername(name))[1]
         and m.user_id = (select auth.uid())
         and m.role = 'admin'
     )
   );
   ```

Permissions per operation: `list` needs `select`; `move` needs `select` and `update`; `remove` needs `select` and `delete`.

## On SupaHost

Don't delete rows from `storage.objects` with SQL to clear a folder. Current Storage versions block direct deletes ("Direct deletion from storage tables is not allowed. Use the Storage API instead."), because removing the row would leave the file on disk. Use the API as shown, from a server with `service_role` for bulk clean-ups.

## Related

- [Storage access control](/docs/storage/access-control)
- [Storage 403 on upload](/kb/storage-rls-upload-403)
- [Cleaning up failed or partial uploads](/kb/storage-partial-uploads)
- [RLS performance best practices](/kb/rls-performance)
