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.tsx in packages/features/accounts).
  • Team logos are set in team settings (update-team-account-image-container.tsx in packages/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:

ActionAllowed for
ReadThe user whose personal account it is, or any member of the team
Upload, replace, deleteThe 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.