Files go to Supabase Storage. The kit uses one bucket, account_image, created in apps/web/supabase/schemas/16-storage.sql.
update-account-image-container.tsx in packages/features/accounts).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.
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.
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.
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.