How to Add Image and File Uploads to a Mobile App
Contents
To add uploads to a mobile app, let the user choose or capture a file, validate it, prepare it locally, upload it to controlled storage, and save a database record that links the object to its owner. The interface also needs progress, cancellation, retry, and cleanup for incomplete uploads.
Images, PDFs, audio, and videos should not all follow the same limits. Define which files each feature accepts before connecting a picker to a storage bucket.
Use the system picker whenever possible, validate files on both the device and backend, keep private content in private buckets, generate ownership-safe object paths, choose resumable uploads for larger files, and track upload state separately from the final content record.
Define the upload before choosing a library
Write a small contract for each upload field. A profile avatar has different rules from a private medical document or a marketplace video.
| Decision | Example for an avatar | Example for a private document |
|---|---|---|
| Allowed source | Camera or photo library | System document picker |
| File types | JPEG, PNG, HEIC if supported | PDF only |
| Maximum size | Small after compression | Larger fixed limit |
| Visibility | Public or authenticated | Private and owner-only |
| Processing | Crop, resize, compress | Malware scan and metadata extraction |
| Retention | Until replaced or account deleted | Product or legal retention rule |
| Failure behavior | Keep previous avatar | Preserve draft and allow retry |
Do not use "any file" as the default. It makes validation, previews, moderation, and storage costs harder to control.
The backend contract should define allowed MIME types, maximum byte size, path ownership, and who can read, replace, or delete the object. The mobile UI can then explain those limits before a user spends time selecting a file that will be rejected.
Choose the correct system picker
For photos and videos, expo-image-picker opens the system interface for selecting media or taking a photo. Expo's ImagePicker documentation covers camera and library selection on Android, iOS, and web.
For PDFs and other documents, expo-document-picker opens the device's available document providers. The DocumentPicker guide describes the supported platforms and returned asset information.
Use a full camera component only for a custom capture experience such as scanning or alignment guides. expo-camera provides the preview and saves captures to the app cache, as described in the Expo Camera documentation.
System pickers reduce the permission scope because the user chooses specific content. Do not request broad media-library access if the picker can complete the feature without it.
Design permission and cancellation states
The upload flow starts before the network request. The user may deny camera access, cancel the picker, choose an unsupported file, or select an asset that is no longer locally available.
Handle each result without treating it as an application error:
| Result | UI response |
|---|---|
| User cancels picker | Return to the previous screen without an error alert |
| Camera permission denied | Explain the feature and offer system settings or library selection |
| File type unsupported | State the accepted formats |
| File too large | Show the maximum size and suggest another file |
| Asset cannot be read | Keep the form and allow another selection |
| Upload cancelled | Remove or retain the pending item according to the product rule |
Ask for camera permission when the user chooses Take Photo, not when the app first launches. Configure the native permission text in the production build so the system prompt explains the real reason.
If the user selects from a limited iOS photo library, be ready for the available assets to change. Avoid assuming that a previously visible library item remains accessible forever.
Validate the selected file locally
Local checks prevent obvious failures and provide fast feedback. They do not replace backend checks because a modified client can bypass them.
Inspect the reported type, extension, byte size, image dimensions or video duration, selection count, filename, and whether the local URI can be read.
Do not rely on the filename to identify content. A file named photo.jpg can contain something else. The backend or processing service should inspect the received bytes when the security risk justifies it.
Generate the final storage path yourself. A safe pattern is:
Do not use the original filename as the only key. Uploads can collide, and filenames may expose personal information. Keep it as controlled metadata only when needed.
Prepare images before upload
Phone photos can be much larger than the version needed for a profile, listing card, or chat preview. Resize and compress them locally when that does not harm the feature.
Expo's ImageManipulator documentation provides local resize, crop, rotate, and format operations.
A practical pipeline reads dimensions and orientation, applies any crop or rotation, resizes to the feature's useful limit, compresses, previews, and uploads.
Keep the original only when the product needs editing, printing, diagnosis, or archival quality. Remove unnecessary photo metadata before public sharing. Process large videos in a backend job and show a clear processing state.
Choose public or private storage deliberately
Public buckets are suitable only when anyone with the URL may view the object. Product screenshots, public avatars, and published listing images may fit that rule. Identity documents, private chat attachments, health files, invoices, and unpublished user content do not.
Supabase explains the difference in its Storage serving guide. Objects in a private bucket require an authenticated request or a time-limited signed URL. Public bucket files are available through public URLs.
| Content | Recommended starting visibility |
|---|---|
| Public profile avatar | Public, if the product clearly makes it public |
| Marketplace listing image | Public after the listing is published |
| Chat attachment | Private |
| Medical or identity document | Private |
| Draft creator content | Private |
| App-owned marketing asset | Public |
A long random public URL is not access control. If the content should be private, put it in a private bucket and authorize access.
Anyone with an active signed URL can use it until expiry, so keep sensitive links short-lived.
Protect storage with row-level policies
Supabase Storage works with Postgres Row Level Security on storage.objects. By default, uploads require an RLS policy. The official Storage access control guide shows how policies can restrict bucket, folder path, owner, and operation.
Create separate rules for the actions your app supports:
| Operation | Example rule |
|---|---|
| Insert | Authenticated user may upload only inside their own folder |
| Select | Owner or authorized resource member may read the object |
| Update | Owner may replace an eligible object |
| Delete | Owner or authorized service may remove the object |
Upsert needs extra permissions and can make accidental replacement easier. Prefer unique object names for user-generated content, then update the database pointer to the new object. Delete the previous object after the replacement is confirmed.
Never put a Supabase service-role key in the mobile app. Service keys bypass RLS and belong only in trusted backend code.
For the wider database setup, see how to connect Supabase to a mobile app and how to add user authentication.
Pick the right upload method
Small images and documents can use a standard upload. Larger files benefit from a resumable protocol that can recover from interrupted mobile connections.
Supabase recommends standard uploads for small files not larger than 6 MB and TUS resumable uploads for files above that point in its standard upload guide. Treat that as provider guidance, then choose a stricter product limit when needed.
| Method | Good for | Main limitation |
|---|---|---|
| Standard upload | Avatars, compressed photos, and small documents | A dropped connection may require restarting |
| Resumable upload | Video, audio, large images, and larger documents | More client and server state |
| Signed upload URL | Direct upload without exposing a privileged backend key | Backend must authorize and issue the path or token |
| Backend-proxied upload | Sensitive processing or strict inspection before storage | Uses backend bandwidth and may add latency |
Supabase's resumable upload guide also supports signed upload tokens for time-limited, authorized upload paths.
Mobile networks, app backgrounding, and processing costs may justify a much smaller product limit than the provider maximum.
Build uploads as a visible state machine
One spinner is not enough when several files can upload or a transfer may take a minute.
Use explicit states:
| State | What the user sees |
|---|---|
| Selected | Filename or thumbnail, size, and remove action |
| Preparing | Compression or local processing progress when available |
| Uploading | Progress percentage, bytes, and cancel action |
| Uploaded | Transfer complete while backend processing may continue |
| Processing | Scan, thumbnail, transcription, or conversion status |
| Failed | Specific reason and Retry action |
| Complete | Final preview or attachment card |
Expo FileSystem provides upload tasks with cancellation and progress subscriptions. The current FileSystem documentation describes creating an upload task and receiving bytes sent and total bytes.
Use one stable upload ID across retries. If the app restarts, save enough pending state to either resume, verify completion, or safely begin again.
Separate the file object from its database record
Object storage holds bytes. Your application database holds product meaning.
An upload record might contain:
| Field | Purpose |
|---|---|
id | Stable upload identifier |
owner_id | Authenticated owner |
bucket and path | Storage location |
original_name | User-facing filename when needed |
mime_type and size_bytes | Validation and display |
status | Pending, uploaded, processing, ready, rejected, or failed |
checksum | Optional integrity or duplicate detection |
resource_type and resource_id | Profile, message, listing, claim, or document relationship |
created_at | Cleanup and audit timing |
Create the application record before upload when the backend must issue an authorized path. Mark it ready only after storage confirms the object and required processing passes.
Do not trust a client request that marks a file scanned or approved. Processing status should be updated by the backend worker that performed the check.
Handle partial failure and cleanup
Uploads cross several systems, so one step can succeed while another fails.
| Failure | Recovery |
|---|---|
| Database record created, upload never starts | Expire and remove stale pending record |
| File uploads, final database update fails | Retry finalization or clean orphaned object later |
| App misses successful response | Verify object or upload record before sending again |
| Processing rejects file | Mark rejected, hide it from normal access, and delete by policy |
| User replaces image | Point to new object first, then remove old object |
| User cancels mid-upload | Abort transfer and clean incomplete session or object |
Run a scheduled cleanup for pending uploads older than a safe threshold and storage objects without valid database references. Log cleanup decisions without storing private file contents.
Use idempotent finalization. Repeating the same completion request should return the same ready record instead of creating duplicate attachments.
Serve images efficiently
Use thumbnails in lists and full images on detail screens. Preserve aspect ratio, show the local preview after selection, and use a placeholder while remote content loads. The expo-image component supports memory and disk caching plus placeholders, according to the Expo Image documentation.
Private image caches should match the content's sensitivity. Clear account-specific local files and memory during account switching when another user must not see them.
Add safety controls for user uploads
User-generated uploads can contain malware, abusive media, personal information, or files designed to exhaust processing resources.
Match controls to the product. Validate size and type on the server, scan documents when needed, limit image pixels and video duration, apply rate and storage quotas, and keep shared content private until it is ready for publishing. Public user content also needs reporting and takedown tools.
Do not render untrusted SVG or HTML inside the app or web view. Keep sensitive filenames, signed URLs, and paths out of logs.
Test uploads on real devices and networks
The happy path on Wi-Fi is not enough. Test:
- Camera permission denied and later enabled.
- Picker cancellation.
- Limited photo access on iOS.
- Unsupported type and oversized file.
- Large photo compression and orientation.
- Slow network, connection change, and airplane mode.
- App backgrounding and process termination during upload.
- Cancel, retry, and repeated retry.
- Duplicate completion requests.
- Private URL opened by another account.
- File replacement and old-object cleanup.
- Logout during an active transfer.
- Several concurrent uploads.
- Backend processing success, rejection, and timeout.
Verify the release build because native permissions and file-provider behavior can differ from a development preview. Use the broader mobile app testing guide before publishing.
Build uploads into your mobile app with Huxly
Huxly helps you build the picker and camera interface, upload progress, Supabase Storage policies, database records, backend processing, and private file access in one mobile app project. You can test the full Expo, Flutter, or SwiftUI flow on real devices and prepare it for TestFlight and Google Play.
FAQ
Should mobile uploads go directly to object storage?
Often, yes, using authenticated storage rules or a signed upload path. Proxy the file through your backend when the content requires immediate inspection, transformation, or a service that does not support safe direct uploads.
Should I use a public or private bucket?
Use public storage only when anyone with the URL may view the file. Start private for chat attachments, documents, drafts, identity data, and other account content.
How can I upload large videos reliably?
Use a resumable upload protocol, persist the upload ID, show progress, support cancellation, and verify completion after reconnecting. Apply product size and duration limits before transfer.
Can I trust the MIME type returned by the mobile picker?
Use it for early client feedback, but validate the received content on the backend when security matters. Filenames and reported types can be incorrect or manipulated.
Should I compress every image?
Compress images when the feature does not need the original resolution. Keep an original or higher-quality version for printing, diagnosis, detailed editing, or archival use.
How do I prevent users from accessing each other's files?
Use owner-based storage policies or a backend authorization check tied to the related resource. A random filename or hidden URL does not provide reliable access control.
Conclusion
Reliable uploads are controlled states, not one network call. Validate the file, prepare it locally, upload it with the right protocol, and link it to an authorized database record.
Private storage rules, progress, retries, idempotent finalization, and orphan cleanup keep the feature working after real mobile interruptions. Once that path is stable, add richer previews and processing without weakening ownership or privacy.



