Supabase Task — Storage Setup
Create the project-attachments private storage bucket with RLS, the task_attachments table, and signed URL generation.
Platform: Supabase Storage
Checklist to Run: Storage Bucket Checklist
Standard: Supabase Standards — Multi-Tenant & Storage
Scenario
The app allows users to upload files to tasks within projects. Each file upload is attached to a specific task. Users should only be able to see and download files for tasks in workspaces they are a member of. File uploads are managed by users — they can upload and delete their own files. Workspace admins can delete any file in the workspace.
What to Build
- A Supabase Storage bucket named
project-attachments(private) - RLS policies on
storage.objectsfor the bucket - A
task_attachmentstable to track file metadata - The upload flow, download flow, and delete flow (describe each — implementation optional)
Requirements
Bucket
- Name:
project-attachments - Visibility: Private (all access via RLS or signed URLs)
- Allowed MIME types:
image/jpeg,image/png,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document - Max file size: 10 MB (enforce in bucket config)
File Path Structure
{workspace_id}/{project_id}/{task_id}/{uuid}-{sanitized_filename}
Example: a1b2c3d4.../e5f6g7h8.../i9j0k1l2.../550e8400-marketing-brief.pdf
- UUID prefix prevents filename collisions
- Sanitized filename: lowercase, spaces replaced with hyphens, no special characters
task_attachments Table
| Column | Type | Notes |
|---|---|---|
| id | uuid | PK, default gen_random_uuid() |
| task_id | uuid | FK → tasks.id, ON DELETE CASCADE |
| workspace_id | uuid | FK → workspaces.id, ON DELETE CASCADE (for RLS) |
| uploaded_by | uuid | FK → auth.users.id |
| storage_path | text | Full path in the bucket |
| filename | text | Original filename for display |
| file_size_bytes | integer | Size in bytes |
| mime_type | text | e.g., 'application/pdf' |
| created_at | timestamptz | default now() |
RLS Policies on storage.objects
SELECT (download): User must be a member of the workspace matching the first segment of the file path.
INSERT (upload): User must be authenticated and a member of the workspace in the first path segment.
DELETE: User uploaded the file (uploaded_by = auth.uid()) OR user is admin/owner of the workspace.
Frontend Upload Flow
- User selects a file — validate type and size before upload begins
- Show upload progress bar
- On upload success: insert a row into
task_attachmentswith the storage path - Show the file in the task's attachment list
- On upload error: show the error, do not insert into
task_attachments
Frontend Download Flow
- User clicks "Download" on an attachment
- Frontend calls
supabase.storage.from('project-attachments').createSignedUrl(path, 300)(5-minute expiry) - Open the signed URL in a new tab
- Never store the signed URL in the database
File Deletion Flow
- User clicks "Delete" on an attachment
- Confirmation dialog: "Delete [filename]? This cannot be undone."
- On confirm: delete the
task_attachmentsrow AND delete the file from Storage - If Storage delete fails: show an error, keep the DB record (no orphaned reference without a file)
What You Should NOT Do
× Make the bucket public (all files contain user data)
× Store signed URLs in the database (they expire and become invalid)
× Store the file as a blob in the database
× Use the service role key in frontend code
× Trust file extension alone for MIME type validation
× Delete the DB record but leave the file in Storage (orphaned files)
× Allow one user's path to be readable by users in a different workspace