The Product: Boom
Boom (App Store, Google Play) is a video-first competition platform. Creators submit clips to monthly contests across 11 categories and users vote for their favorites. A jury then selects a winner in each category. Each winner receives a substantial cash prize.
At its core, Boom is about competitions, not just social sharing. Uploading a video is not a side feature; it is how creators enter contests. That's why a reliable and smooth upload flow is critical to the experience.
The Challenge: Large Video Uploads on Mobile
Our initial implementation for mobile video uploads used a straightforward JavaScript uploader. It worked for our MVP, but did not scale beyond it. It had several critical flaws:
- Lack of Background Execution: JavaScript execution is tied to the app's lifecycle. If the app was backgrounded, the upload would be suspended and often terminated by the OS.
- Handling Network Interruptions: A single network blip could cause the entire upload to fail, forcing the process to restart.
The Goal: Fast and Reliable Mobile Video Uploads
Our goal was to create an upload experience that was:
- Fast: Transfer videos from a user's device to servers as quickly as possible.
- Reliable: Uploads should survive app switching, network hiccups and other interruptions.
The Solution: Native Background Uploads with Expo Modules
We built a brand-new upload pipeline using Expo Modules. With a SharedObject, we created a long-lived, stateful upload task. By switching to AWS S3 multipart upload, large videos now upload faster and more reliably. This eliminates failed uploads and the need to restart.
Here's the TypeScript interface for our UploadTask:
export type UploadTaskEvents = {
onProgress: (params: { progress: number }) => void
}
export declare class UploadTask extends SharedObject<UploadTaskEvents> {
constructor(clipPath: string, coverPath: string)
readonly parts: URL[]
clipUrls?: string[]
completionUrl?: string
coverUrl?: string
preProcess(): void
upload(): Promise<void>
}The iOS native module that exposes the UploadTask class to the JavaScript side.
import ExpoModulesCore
public class BackgroundUploadModule: Module {
public func definition() -> ModuleDefinition {
Name("BackgroundUpload")
Class(UploadTask.self) {
Constructor { (clip: URL, cover: URL) -> UploadTask in
return UploadTask(clip: clip, cover: cover)
}
Property("parts") { uploadTask in
uploadTask.clip.parts
}
Property("clipUrls") { uploadTask in
uploadTask.clip.uploadUrls
}
.set { (uploadTask: UploadTask, uploadUrls: [URL]) in
uploadTask.clip.uploadUrls = uploadUrls
}
Property("completionUrl") { uploadTask in
uploadTask.clip.completionUrl
}
.set { (uploadTask: UploadTask, completionUrl: URL) in
uploadTask.clip.completionUrl = completionUrl
}
Property("coverUrl") { uploadTask in
uploadTask.cover.uploadUrl
}
.set { (uploadTask: UploadTask, uploadUrl: URL) in
uploadTask.cover.uploadUrl = uploadUrl
}
Function("preProcess") { uploadTask in
try uploadTask.preProcessAssets()
}
AsyncFunction("upload") { uploadTask in
try await uploadTask.upload()
}
}
}
}React Native Integration Example
Below is a simplified React Native integration example. The native module handles chunking, concurrency, retries and background execution.
// instantiate an UploadTask that owns the parts, progress, and URLs
const uploadTask = new BackgroundUpload.UploadTask(videoUri, thumbnailUri)
// listen for progress
const progressListener = uploadTask.addListener('onProgress', ({ progress }) => {
updateProgress({ uploadProgress: progress })
})
// split the file into independent parts
uploadTask.preProcess()
// request the backend for presigned URLs
const { data: uploadInfo } = await createUploadUrls({
variables: { input: { partsCount: uploadTask.parts.length } },
})
// configure task
uploadTask.clipUrls = uploadInfo.clip.uploadUrls.map(({ uploadUrl }) => uploadUrl)
uploadTask.completionUrl = uploadInfo.clip.completionUrl
uploadTask.coverUrl = uploadInfo.cover.uploadUrl
// begin upload parts in parallel with retries
try {
await uploadTask.upload()
} catch(error) {
logError(error)
} finally {
progressListener.remove()
}Results: Faster, Smaller, More Reliable Uploads
- Speed: Median end-to-end upload time for mobile video uploads improved by ~20% for 100-300MB clips.
- Reliability: No stuck uploads observed in our latest test runs.
Measurement: End‑to‑end = tap Agree and Post → backend confirms completion.
Conclusion: Why Expo Modules are the Best Way to Add Native Uploads
For teams looking to improve native mobile performance, Expo Modules provide an effective way to combine JavaScript flexibility with native capability.
Compared to TurboModules, they're easier to maintain, require less boilerplate, and integrate smoothly with Expo projects. This lowers the long-term maintenance cost while giving us native performance where it matters. The investments delivered a smoother user experience and a more robust, reliable app.








