Storage
The upload sample, Supabase Storage bucket, and media picker flow.
The upload sample is an Example feature: it shows a complete Android media picker -> Supabase Storage upload flow, but it is intentionally simple enough to replace with your own product-specific file model.
What it provides
- Android photo/video picker via
ActivityResultContracts.PickMultipleVisualMedia. - A private Supabase Storage bucket named
sample-uploads. - Owner-scoped object paths under
<user-id>/. - Sequential uploads with per-job progress from
uploadAsFlow. - In-memory queue state for pending, uploading, complete, and failed jobs.
The Kotlin template does not use the Expo sample's sample_uploads metadata
table. The UI tracks the current upload queue in memory.
Key files
Upload flow
Pick images or videos
UploadScreen creates a launcher with:
ActivityResultContracts.PickMultipleVisualMedia(MAX_PICK_COUNT)It launches with PickVisualMedia.ImageAndVideo, so both images and videos are
accepted. The screen reads display names, MIME types, and sizes through
ContentResolver.
Require a signed-in user
UploadViewModel reads sessionController.currentUser.value?.id. If there is
no user, it shows "Sign in to upload files." and does not touch Storage.
Validate size and bytes
Each file is capped at 25 MB. Empty files fail before upload.
Stream upload progress
UploadsRepository.upload() writes to the sample-uploads bucket and returns
Flow<UploadStatus>. The ViewModel maps UploadStatus.Progress into row
progress and UploadStatus.Success into a completed row with the final storage
path.
Object path convention
UploadsRepository writes paths like:
<user-id>/<epoch-millis>-<sanitized-file-name>The file-name sanitizer keeps only letters, numbers, ., _, and -, turns
other runs into -, trims edge dashes, and falls back to upload when the name
becomes blank.
Example:
11111111-1111-1111-1111-111111111111/1782940800000-vacation.movThe folder-scoped Storage policies below depend on the first path segment being the Supabase user id.
Create the bucket and policies
Run this SQL after creating your Supabase project:
insert into storage.buckets (id, name, public, file_size_limit)
values ('sample-uploads', 'sample-uploads', false, 26214400)
on conflict (id) do update
set public = excluded.public,
file_size_limit = excluded.file_size_limit;
do $$
begin
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can read own sample upload objects'
) then
execute $policy$
create policy "Users can read own sample upload objects"
on storage.objects for select
to authenticated
using (
bucket_id = 'sample-uploads'
and (storage.foldername(name))[1] = auth.uid()::text
)
$policy$;
end if;
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can upload own sample upload objects'
) then
execute $policy$
create policy "Users can upload own sample upload objects"
on storage.objects for insert
to authenticated
with check (
bucket_id = 'sample-uploads'
and (storage.foldername(name))[1] = auth.uid()::text
)
$policy$;
end if;
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can update own sample upload objects'
) then
execute $policy$
create policy "Users can update own sample upload objects"
on storage.objects for update
to authenticated
using (
bucket_id = 'sample-uploads'
and (storage.foldername(name))[1] = auth.uid()::text
)
with check (
bucket_id = 'sample-uploads'
and (storage.foldername(name))[1] = auth.uid()::text
)
$policy$;
end if;
if not exists (
select 1 from pg_policies
where schemaname = 'storage'
and tablename = 'objects'
and policyname = 'Users can delete own sample upload objects'
) then
execute $policy$
create policy "Users can delete own sample upload objects"
on storage.objects for delete
to authenticated
using (
bucket_id = 'sample-uploads'
and (storage.foldername(name))[1] = auth.uid()::text
)
$policy$;
end if;
end $$;The bucket limit matches the app's 25 MB check. If you raise one, raise the other.
Account deletion caveat
The current Kotlin delete-account implementation does not remove
sample-uploads objects. It deletes todos, nulls profile fields, and signs
out locally; the auth user and storage files remain.
The recommended template fix is the same one described in
Authentication: add a service-role
delete-account Edge Function that removes sample-uploads/<user-id>/ objects
before deleting the Supabase Auth user.
Display and customize
The sample upload screen reports the final storage path but does not render a
remote gallery. If your app needs previews, use Coil 3 (AppAsyncImage already
wraps Coil for the UI kit) and decide whether your bucket should be private with
signed URLs or public with CDN-backed object URLs.
To make this production-specific:
- Rename
UploadsRepository.BUCKET_ID. - Change the object path convention if your product needs albums, teams, or object ids as path segments.
- Add a metadata table if files need titles, ownership beyond one user, review states, or listing/history.
- Keep Storage policies in lockstep with the path shape.
Verify
-
sample-uploadsexists and is private. - A signed-in user can upload an image below 25 MB.
- The returned storage path starts with that user's UUID.
- Another signed-in user cannot read or overwrite that object.
- A signed-out user cannot upload.
