File uploads
Profile pictures and team logos in Supabase Storage, the policies that guard them, and how to add a private bucket for your own files.
Files go to Supabase Storage. The kit uses one bucket, account_image, created in apps/web/supabase/schemas/16-storage.sql.
Profile pictures and team logos
- Users set their picture in personal account settings (
update-account-image-container.tsxinpackages/features/accounts). - Team logos are set in team settings (
update-team-account-image-container.tsxinpackages/features/team-accounts).
Each file is named after the account id it belongs to (the user's id for a personal account, the team's id for a team), uploaded with upsert, and its public URL is saved to accounts.picture_url with a ?v= value that changes on every upload so browsers fetch the new image.
Who can do what
The policies on storage.objects read the account id from the file name:
| Action | Allowed for |
|---|---|
| Read | The user whose personal account it is, or any member of the team |
| Upload, replace, delete | The user whose personal account it is, or a team member with the settings.manage permission |
Each action has its own policy, because a single policy for all commands would let any member delete the team logo (delete checks only the USING clause). storage.test.sql tests these rules, including a user trying to reach another account's file.
The bucket is public: anyone who has a file's URL can load it, signed in or not. That suits avatars and logos. Do not put private files in it.
Your own files
For documents, exports or anything private, create a private bucket in a migration and write policies against the account the file belongs to. One way is to start each file path with the account id:
insert into storage.buckets (id, name, public)
values ('documents', 'documents', false);
create policy documents_read on storage.objects for select
to authenticated using (
bucket_id = 'documents'
and (
(storage.foldername(name))[1]::uuid = auth.uid()
or public.has_role_on_account((storage.foldername(name))[1]::uuid)
)
);
Add matching insert, update and delete policies, then a pgTAP test that signs in as an outsider and tries to read and write. Serve private files through signed URLs, not public ones.
For the upload UI, @repo/ui/file-uploader exports FileUploader, a drop zone that uploads straight to a bucket and takes bucketName, path, allowedMimeTypes, maxFileSize and maxFiles.
Size limit
The local stack allows files up to 50 MiB (file_size_limit in apps/web/supabase/config.toml). Set the limit for your hosted project in the Supabase dashboard.