AWS S3 File Storage Integration in Mobile Applications
When user uploads avatar or document via mobile app, first question is where to send it. Storing binaries in database is crazy. Routing through own backend means running megabytes through extra hop and paying for traffic twice. AWS S3 with presigned URL solves this elegantly: client uploads directly to S3, backend only issues temporary token.
How Proper Integration Works
Standard scheme: mobile client requests presigned URL from backend, gets it, makes PUT request directly to S3, then notifies backend file is uploaded. Backend never sees file bytes — only metadata.
iOS uses AWS SDK for Swift (aws-sdk-swift) or lighter variant — AWSS3 via Amplify. Android uses AWS SDK for Kotlin or Amplify Android. React Native uses @aws-sdk/client-s3 from official JS SDK v3.
// iOS: upload via presigned URL without SDK — via URLSession
let presignedURL = URL(string: urlFromBackend)!
var request = URLRequest(url: presignedURL)
request.httpMethod = "PUT"
request.setValue("image/jpeg", forHTTPHeaderField: "Content-Type")
let uploadTask = URLSession.shared.uploadTask(with: request, fromFile: localFileURL) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
// retry logic here
return
}
// Notify backend of successful upload
self.confirmUpload(fileKey: fileKey)
}
uploadTask.resume()
On Android via OkHttp or Retrofit — similar. Main thing — don't forget Content-Type header: S3 checks it matches what was specified when generating presigned URL. Mismatch gives 403.
Common Problem Areas
Presigned URL expiry. Default is 15 minutes. If user on slow connection or app waits long for file picker, URL expires before upload starts. Practice: generate URL right before PUT request, not when picker opens.
CORS for web preview. If same files shown in WebView or website, S3 bucket needs CORS policy. Without it browser blocks request, though mobile client works fine — URLSession and OkHttp don't check CORS.
Multipart for large files. S3 Multipart Upload needed from 5 MB. For video this is mandatory. AWS Amplify Storage abstracts this automatically via Amplify.Storage.uploadFile, but with manual presigned URL implementation multipart requires separate API: CreateMultipartUpload → UploadPart × N → CompleteMultipartUpload.
// Android: using Amplify for automatic multipart
Amplify.Storage.uploadFile(
StoragePath.fromString("uploads/${userId}/${filename}"),
localFile,
StorageUploadFileOptions.builder()
.contentType("video/mp4")
.build(),
progress -> Log.i("Upload", "Progress: ${progress.fractionCompleted}"),
result -> confirmUpload(result.path),
error -> handleUploadError(error)
)
Access rights via IAM. Bucket policy must allow s3:PutObject only for specific prefixes. No wildcard s3:* on entire bucket in production. Use Resource-based policy + IAM role with minimal rights.
File Structure and Lifecycle
Well-planned S3 key structure saves headaches later:
uploads/{userId}/avatar/{timestamp}.jpg
uploads/{userId}/documents/{uuid}.pdf
public/content/{category}/{slug}.webp
temp/{sessionId}/{filename} ← cleaned via lifecycle rule in 24h
S3 Lifecycle Rules automatically delete temp files and move rarely-used to S3 Glacier for savings. Configured via Terraform or AWS Console — nothing in app code.
CloudFront Before S3
Serving media directly from S3 is normal for start. But for production with users in different regions, putting CloudFront is worth it. This CDN with edge nodes caches files closer to user and reduces latency on image loads. On iOS noticeable on thumbnail list loads: without CDN each request goes to us-east-1, with CDN from nearest edge.
For presigned URL with CloudFront need CloudFront Signed URL (not S3 presigned) — different mechanism with different signing logic.
Timeline
Basic integration (presigned upload + download, one platform): 3–5 days. Full implementation with multipart, progress tracking, retry, lifecycle rules, CloudFront: 2–3 weeks. Cost estimated individually.







