# Pattern 16 — Team-shared storage bucket

## What this is

A storage bucket where all members of an org can read and write files, isolated to their org's prefix. Builds on the tenant isolation concepts from patterns 09-12 and applies them to Supabase Storage's path-based model.

## Path convention

```
team-files/<org-id>/design/brand-guidelines.pdf
team-files/<org-id>/exports/2026-Q2.csv
team-files/<org-id>/uploads/client-photo.jpg
```

The first path segment is the org ID. All policies check `(storage.foldername(name))[1]` against the user's org. You can add any subdirectory structure under the org prefix without changing the policies.

## The owner field for DELETE

Supabase Storage automatically populates `storage.objects.owner` with `auth.uid()` when a file is uploaded. This gives you a built-in way to identify who uploaded each file.

Without an ownership check on DELETE, any team member can delete any other member's files:

```sql
-- Dangerous: any org member can delete any file in the org
create policy "bad_delete"
  on storage.objects for delete to authenticated
  using (
    bucket_id = 'team-files'
    and (storage.foldername(name))[1] = (select org_id::text from users where id = auth.uid())
    -- Missing: and owner = auth.uid()
  );
```

The correct pattern adds `and owner = auth.uid()` for the member delete policy, and a separate admin policy for org-wide deletion.

## Subdirectory structure is free

Because the policy only checks `foldername(name)[1]`, you get subdirectory isolation for free:

```ts
// All three paths are accessible to org members and isolated from other orgs
await supabase.storage.from('team-files').upload(`${orgId}/design/logo.svg`, file);
await supabase.storage.from('team-files').upload(`${orgId}/exports/report.csv`, file);
await supabase.storage.from('team-files').upload(`${orgId}/uploads/photo.jpg`, file);
```

## Testing

```sql
-- Confirm org isolation: user from org A cannot access org B's files
set request.jwt.claims = '{"sub": "<user-a-id>"}';
select name from storage.objects
where bucket_id = 'team-files'
and (storage.foldername(name))[1] = '<org-b-id>';
-- Should return 0 rows

-- Confirm delete ownership: user B cannot delete user A's file
set request.jwt.claims = '{"sub": "<user-b-id>"}';
delete from storage.objects
where name = '<org-id>/user-a-file.pdf' and owner = '<user-a-id>';
-- Should delete 0 rows (silently blocked)
```
