Key takeaways
- Uppy handles file selection and upload progress; its Transloadit plugin connects uploads to managed validation, processing, and export to your own storage.
- Treat resumability, retry, idempotency, and expiry as separate reliability controls with different failure behavior.
- Keep upload authorization on the server, validate observed file properties, and publish only from controlled permanent storage.
A file upload API should connect authenticated upload → validation → processing → customer-owned storage. This guide implements that path for image uploads: Uppy provides the upload experience, Transloadit runs the managed processing workflow, and your S3 bucket stores the accepted original and preview. Uppy also works independently of Transloadit; your application remains responsible for user authentication and deciding when an asset is ready to publish.
What matters most
- Choose build, buy, or hybrid ownership by testing recovery, security, processing, and operational requirements—not only the happy-path demo.
- Use framework-specific examples only when their lifecycle, server authorization, and recovery behavior can be maintained and tested.
Define the upload job before choosing a widget
A production upload begins before the first byte moves. The application identifies the user, decides which operation is allowed, describes acceptable file count and size, and creates a record that can outlive a browser tab. Transfer is only one stage. Validation, processing, export, notification, and reconciliation determine whether the product can safely use the file afterward.
Write a completion contract in product terms. “The request returned 200” is weak; “the original and required derivatives are stored under this tenant, the asset record names their versions, and a duplicate callback changes nothing” is testable. Record which failures users may retry, which require a new upload, and which leave an operator-visible state for recovery.
Control plane
Carries identity, authorization, limits, workflow choice, metadata, status, and result references rather than the file body itself.
Data plane
Carries file bytes between the user, application, upload service, processing layer, and durable storage destination.
Trust transition
Marks when an untrusted upload has passed the checks required for processing, storage, preview, or public delivery.
Choose one of four explicit byte paths
In an application relay, the browser sends the file to your server and your server forwards or stores it. This is understandable and gives the application immediate control, but every byte consumes your ingress, memory or temporary disk, connection time, and egress. It fits small, infrequent files when the existing server can enforce limits and stream safely without buffering entire bodies.
“Direct to cloud” is ambiguous, so name the destination. A browser can upload directly to object storage with short-lived credentials, directly to a processing service such as Transloadit, or ask a service to import an existing remote URL. Direct storage minimizes hops when persistence is the only job. Direct processing keeps the application out of the data path while a workflow validates, transforms, and exports files to owned storage.
Application relay
Useful for modest workloads and simple policy, but the application owns transfer capacity, timeouts, temporary files, and scaling.
Direct object storage
Best when the first durable copy is the main outcome and later processing can be triggered reliably from a storage event or queue.
Direct processing service
Useful when upload and asynchronous validation, derivatives, metadata, or multi-destination export belong to one observable job.
Remote-source import
Moves bytes server to server, which saves the user’s connection but requires explicit source authorization and fetch limits.
Design resumability separately from retries
A retry starts an operation again; a resumable transfer continues an existing upload from a server-confirmed byte offset. With tus, the client retains the upload URL, asks the server for Upload-Offset, and sends only the remaining bytes. Persist that URL outside transient component state if a refresh or crash should recover, and fingerprint files carefully so one user’s local file is never attached to another upload resource.
Resumability does not make time unlimited or processing idempotent. A Transloadit Assembly still has eight hours from creation to finish uploading, and creating a replacement Assembly can duplicate work unless the application reconciles the old identifier. Define how the client handles pause, offline time, expired resources, changed files, abandoned uploads, and a completion response lost after the server accepted the final bytes.
Resume identity
Persist the server-issued upload URL together with the authenticated user, local file fingerprint, expected length, and operation record.
Expiry path
When the upload resource or Assembly has expired, create a fresh operation and retire the stale identifier instead of retrying forever.
Completion reconciliation
Query durable status after ambiguous network failures so the client does not assume that a missing response means missing bytes.
Authorize intent on the server and distrust the bytes
Browser code may contain a public Auth Key, but it must never contain the Transloadit Auth Secret or permanent storage credentials. Authenticate the user in your application, select a saved Template on the server, and return short-lived signed parameters with a unique nonce. Set allow_steps_override to false when the browser has no legitimate reason to replace Steps, because a client-selected Step graph could otherwise change processing or export behavior.
A valid signature proves that the parameter payload was authorized; it does not prove that the uploaded bytes match a filename, extension, declared MIME type, tenant, or moderation policy. Limit request bodies before expensive work, inspect observed file properties, reject unsupported content, scan where the threat model requires it, and keep untrusted output away from public storage until the workflow reaches an approved state.
Short-lived authorization
Issue upload permission only after application authentication and scope it to a server-selected operation for a limited time.
Observed properties
Use detected type, dimensions, duration, and other inspected metadata for routing instead of trusting the extension alone.
Quarantine before publish
Separate receipt from public delivery so invalid, malicious, or policy-rejected files never become an application asset by default.
Separate durable storage from upload processing
An upload endpoint is not automatically a system of record. Decide which bucket or asset database owns the original, how derivatives relate to it, which identifiers survive renames, and who deletes each copy. Transloadit retains temporary results for at least 24 hours, while their access URLs can expire after a few hours. Export files that must persist. Temporary URLs are limited to short-term retrieval, not embedding or repeated product delivery.
Place export inside the Assembly when workflow success requires both processing and persistence. An export Robot can use stored Template Credentials to write results to the chosen destination as part of the workflow. Alternatively, upload directly to owned storage first and invoke processing from a controlled event. That path gains an early durable copy but adds orchestration and another transfer into the processor.
Original ownership
State whether the original is retained, for how long, under which tenant key, and whether later workflows may read it again.
Derivative lineage
Store the source identifier, workflow configuration, output role, dimensions, format, and checksum needed to explain each result.
Delivery boundary
Serve approved assets from permanent storage and an intentional delivery layer rather than temporary processing URLs.
Make the build-versus-buy decision at the ownership boundary
Build the transfer path when requirements are narrow and the team is prepared to own the complete lifecycle. A small authenticated form that streams short files into one existing bucket may not justify another platform. The estimate must still include multipart parsing, backpressure, size enforcement, resumability or its deliberate absence, cleanup, abuse controls, observability, upgrades, and support for failures outside the request’s lifetime.
A managed service becomes more attractive as the workflow combines unreliable networks, large files, browser UX, remote sources, media inspection, transformation, or several storage destinations. Buying does not remove application ownership: tenant checks, authorization, asset records, retention, publication, and incident handling remain yours. A hybrid design often works best, with owned storage and business state around a managed transfer and processing layer.
Build cost
Count engineering, infrastructure, on-call work, protocol maintenance, security review, and user support—not only object-storage fees.
Buy cost
Model upload bytes, processing operations, retries, storage transfer, minimum charges, support level, and expected growth.
Hybrid ownership
Keep identity, policy, metadata, and permanent storage in your product while delegating the specialized data path and processing work.
Compare providers with failure tests, not feature counts
Create a scorecard from the product’s actual workload. Compare browser and mobile clients, open protocol support, maximum file size, concurrency behavior, geographic endpoints, remote imports, processing breadth, storage destinations, credential isolation, webhook verification, status retention, support, and exit options. Mark every feature as required, optional, or irrelevant before looking at vendor pages.
Run the same fixtures through each serious candidate. Interrupt a large upload, reload the page, send a duplicate completion event, revoke storage credentials, decline a file after receipt, exceed a limit, and lose the final response. Measure user-visible recovery, bytes retransmitted, time to durable output, operator evidence, and cleanup. A polished picker says little about these production properties.
Protocol portability
An open resumable protocol and replaceable clients reduce migration coupling, but workflow and result schemas still require migration planning.
Operational evidence
Require stable job identifiers, terminal states, timestamps, actionable errors, verified callbacks, and a documented replay procedure.
Complete economics
Compare transfer, processing, storage, delivery, support, engineering, and failure-recovery costs at representative monthly volume.
Implement a signed Uppy and Transloadit upload
The browser example lets Uppy manage selection and tus transfer while the Transloadit plugin requests Assembly parameters from your application. The server endpoint must authenticate the current user before returning the object from calcSignature. It should select the Template itself instead of accepting arbitrary Steps or a caller-provided storage destination, and it should rate-limit authorization independently from upload traffic.
Before using this example, enable “Require a correct Signature” in Workspace Settings, save the Template below, and set its ID in the server’s TRANSLOADIT_UPLOAD_TEMPLATE_ID. Store AWS access to your own private bucket as Template Credentials named my_s3_credentials. The Auth Key may reach the browser; the Auth Secret and AWS credentials must not. The signing endpoint must return Cache-Control: no-store so each authorized operation receives fresh parameters.
Install @uppy/core, @uppy/dashboard, and @uppy/transloadit in the frontend and the transloadit Node SDK on the server. Mount the browser example after an element such as <div id="photo-upload"></div> exists. Implement /api/transloadit-params in your framework with session authentication and upload authorization before calling the signing helper. Reject unauthenticated or unauthorized requests; the helper itself is not an authentication endpoint.
The example permits one JPEG, PNG, or WebP image up to 10 MiB. Uppy’s restrictions provide immediate feedback, while the signed auth.max_size and auth.max_number_of_files limits and the Template’s server-side filter enforce the policy. waitForEncoding: true waits for the Assembly, including export, instead of treating transfer completion as workflow completion.
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import Transloadit from '@uppy/transloadit'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
// Mount this once after <div id="photo-upload"></div> exists in your page.
const uppy = new Uppy({
restrictions: {
maxNumberOfFiles: 1,
maxFileSize: 10 * 1024 * 1024,
allowedFileTypes: ['image/jpeg', 'image/png', 'image/webp'],
},
}).use(Dashboard, { inline: true, target: '#photo-upload' }).use(Transloadit, {
async assemblyOptions() {
const response = await fetch('/api/transloadit-params', {
credentials: 'same-origin',
cache: 'no-store',
})
if (!response.ok) {
throw new Error('Could not authorize this upload')
}
return response.json()
},
waitForEncoding: true,
retryDelays: [0, 1000, 3000, 5000, 10000],
})
uppy.on('transloadit:assembly-created', (assembly) => {
// Associate this ID with the server-side operation before the user leaves the page.
console.log('Assembly started:', assembly.assembly_id)
})
uppy.on('transloadit:complete', (assembly) => {
// Record completion against the ID persisted at transloadit:assembly-created.
console.log('Processing completed. Assembly:', assembly.assembly_id)
})
uppy.on('transloadit:assembly-error', () => {
// Show this through the application’s accessible status UI, not raw API errors or URLs.
console.error('Processing failed. Check the Assembly in your workspace.')
})
uppy.on('upload-error', () => {
// Transloadit API signature rejections and Assembly errors also reach this event.
// Failures thrown by assemblyOptions() use Uppy’s general error event instead.
// Deduplicate application notices.
console.error('The upload workflow failed. Check its status before retrying.')
})import { randomUUID } from 'node:crypto'
import { Transloadit } from 'transloadit'
function requiredEnvironmentValue(name: string): string {
const value = process.env[name]
if (value == null) throw new Error(`Missing environment variable: ${name}`)
return value
}
const transloadit = new Transloadit({
authKey: requiredEnvironmentValue('TRANSLOADIT_KEY'),
authSecret: requiredEnvironmentValue('TRANSLOADIT_SECRET'),
})
export function createAuthorizedUploadParameters(): { params: string; signature: string } {
// Call this only after the server has authenticated the request and authorized the operation.
const params = {
auth: {
expires: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
max_size: 10 * 1024 * 1024,
max_number_of_files: 1,
nonce: randomUUID(),
},
template_id: requiredEnvironmentValue('TRANSLOADIT_UPLOAD_TEMPLATE_ID'),
}
return transloadit.calcSignature(params)
}Connect intake, validation, processing, and export
One Assembly connects the complete path: authenticated Uppy upload → /file/filter validation → /image/resize processing → /s3/store export to a customer-owned bucket. Its saved Template supplies the workflow, and each use dependency determines which files reach the next Step. Uppy supplies the upload experience; Transloadit executes the managed workflow, not your application server.
The filter checks detected MIME type and file size, then passes only accepted images to the preview and export Steps. error_on_decline: true makes a rejection an Assembly error. MIME and size checks are not malware scanning or content moderation; add those Steps before processing and export when your policy requires them.
Use a private S3 bucket with Block Public Access enabled and appropriately scoped Template Credentials. acl: "bucket-default" omits an object ACL and relies on the bucket’s access policy; it does not make a public bucket private. The export stores the accepted original and a preview bounded by 1600 × 1600 pixels under Assembly- and file-specific paths. Neither export reads directly from unfiltered :original.
{
"allow_steps_override": false,
"steps": {
":original": {
"robot": "/upload/handle"
},
"accepted_images": {
"use": ":original",
"robot": "/file/filter",
"accepts": [
["${file.mime}", "regex", "^image/(jpeg|png|webp)$"]
],
"declines": [["${file.size}", ">", 10485760]],
"error_on_decline": true
},
"preview": {
"use": "accepted_images",
"robot": "/image/resize",
"resize_strategy": "fit",
"width": 1600,
"height": 1600
},
"exported": {
"use": ["accepted_images", "preview"],
"robot": "/s3/store",
"credentials": "my_s3_credentials",
"acl": "bucket-default",
"path": "uploads/${assembly.id}/${file.id}/${file.url_name}"
}
}
}Record the operation
Capture the Assembly ID when transloadit:assembly-created fires, not only in the completion callback. Associate it with the authenticated user and upload record on your server; do not treat a browser-supplied ID as proof of ownership.
Confirm durable completion
Reconcile status on your server or verify an Assembly Notification signature. Require ASSEMBLY_COMPLETED and both expected outputs in results.exported before marking the asset ready. Store their permanent object references, not temporary processing URLs; a private object URL still needs authorized delivery.
Handle partial failure
Keep failed operations unpublished and make repeated completion notifications harmless. The original may export before the preview finishes, so an Assembly error does not mean the bucket is empty. Reconcile or clean up partial objects before retrying.
Test a large video from interrupted upload to private export
For a video workflow, create a Template Credential named large-upload-output using IAM credentials whose s3:PutObject permission is limited to the private upload-tests/ prefix. Follow the /s3/store IAM setup for bucket-level s3:ListBucket and s3:GetBucketLocation permissions; the location lookup is unnecessary when the Template Credential supplies bucket_region. Replace YOUR_AUTH_KEY with the workspace’s Auth Key (not its Auth Secret), save the Template below, and require Signature Authentication on that Template. Use a private bucket with Block Public Access enabled and Object Ownership set to Bucket owner enforced. The acl: "bucket-default" setting omits an object ACL; access remains controlled by your bucket and IAM policies. Set the authenticated signing endpoint’s TRANSLOADIT_UPLOAD_TEMPLATE_ID to this saved video Template’s ID for the test. Reuse the Uppy integration above with a user-selected file and one Uppy instance. The Template’s 256 MiB and one-file limits are example policy. Mirror those limits in the Uppy core restrictions option (maxFileSize, maxNumberOfFiles) for early picker feedback; server-side checks remain necessary. For this video test, replace the image-only restriction with allowedFileTypes: ["video/*"] and set both the browser’s maxFileSize and the signed auth.max_size to 256 * 1024 * 1024; keep both file-count limits at 1. Check your workspace’s upload limits and supported source formats before testing. The Template detects a video MIME family, creates a bounded MP4 rendition, and exports that rendition and the accepted original. This is not a complete malware or content-safety policy.
Transfer completion is not the finish line. With waitForEncoding: true, the browser waits for processing, but your application still needs a durable Assembly ID and verified notifications or an Assembly Status lookup if the tab disappears. An export failure must not mark the asset ready. Confirm ASSEMBLY_COMPLETED, the required rendition, and both private S3 objects; compare the exported original’s checksum with the input before recording success. These private objects are not automatically public playback URLs.
Run three online controls and three interrupted runs using the same owned 100–200 MiB video. Record the exact file bytes and SHA-256, duration and codecs, browser and package versions, Template, region, plan, and network setup. In Chrome DevTools, apply a custom throttling profile and record its settings, switch to Offline near 25% uploaded for 10 seconds, and restore the profile without reloading. For network failures reported while the browser is offline and retry attempts remain, the installed Uppy tus plugin pauses its queue until an online event. Each retry still consumes an attempt; upload progress can reset the counter. The sum of retryDelays is not an offline time limit. Verify that a HEAD request reports the saved Upload-Offset and subsequent PATCH requests continue the same tus resource. This exercises an emulated browser interruption, not server failover or every network condition.
Measure from upload initiation to the first completed Assembly with verified exported objects, not just to the last uploaded byte. Also record reconnect-to-completion time, retransmitted bytes where observable, failures, duplicate Assemblies, and the raw run count. This is a reproducible test procedure, not a published benchmark result. Bounded retries can be exhausted by repeated request failures, the eight-hour Assembly upload deadline still applies, and this in-memory example does not restore state after a reload or closed tab. Test cancellation, expired authorization, over-limit files, and revoked export credentials separately. See the resumable-upload API for the protocol handoff and the video and S3 demo for a processing and public-export example.
{
"allow_steps_override": false,
"auth": {
"key": "YOUR_AUTH_KEY",
"max_size": 268435456,
"max_number_of_files": 1
},
"steps": {
":original": { "robot": "/upload/handle" },
"accepted_video": {
"use": ":original",
"robot": "/file/filter",
"accepts": [["${file.mime}", "regex", "^video/"]],
"error_on_decline": true
},
"rendition": {
"use": "accepted_video",
"robot": "/video/encode",
"ffmpeg_stack": "v7",
"preset": "web/mp4/360p",
"width": 640,
"height": 360,
"resize_strategy": "fit",
"result": true
},
"exported": {
"use": ["accepted_video", "rendition"],
"robot": "/s3/store",
"credentials": "large-upload-output",
"acl": "bucket-default",
"path": "upload-tests/${assembly.id}/${file.id}/${file.url_name}"
}
}
}Technical details worth knowing
- A browser-to-application upload makes the application server part of the data path, while a direct-storage or direct-processing design keeps file bytes out of that request handler.
- The tus core protocol resumes by reading the server’s
Upload-OffsetwithHEADand continuing withPATCH; sending the whole file again is a retry, not a resume. - Uppy’s Transloadit plugin uses tus for file transfer and can request signed Assembly parameters from an application back end through its
assemblyOptionsfunction. - Transloadit Signature Authentication signs the JSON-encoded parameters with the Auth Secret on a trusted server; the secret itself must never be sent to browser code.
- A saved Template with
allow_steps_overrideset to false prevents an untrusted client from replacing its Steps or selecting a different storage target through Step overrides. - Export files that must persist: temporary results are retained for at least 24 hours, but their URLs can expire after a few hours and are intended only for limited, short-term retrieval.
- For Transloadit tus uploads, the Assembly is created before file bytes arrive and remains in
ASSEMBLY_UPLOADINGuntil the declared uploads have finished. - The Transloadit upload window is eight hours from Assembly creation, so a resumable client still needs a deliberate restart path when the Assembly has expired.
A practical approach
- 1
Write the byte path, trust transitions, durable owner, and completion contract before selecting an uploader.
- 2
Test relay, direct-storage, and direct-processing options against representative files and network failures.
- 3
Implement short-lived server authorization, resumability, validation, export, and idempotent result handling.
- 4
Load-test the chosen path and rehearse expiry, duplicate callbacks, revoked credentials, and partial failures.
When Transloadit is useful
Use Transloadit when uploads need a managed workflow connecting resumable transfer, server-side validation, processing, and exports to storage you control. Exact parameter contracts live in the /upload/handle, /file/filter, /image/resize, /video/encode, and /s3/store Robot documentation.
Architecture boundary
Transloadit can receive files, run asynchronous processing workflows, and export results, but your application still owns user authentication, tenant authorization, the durable asset record, publication policy, and delivery from permanent storage.
Frequently asked questions
What does “direct-to-cloud upload” mean?
It is not one architecture. It can mean browser-to-object-storage, browser-to-processing-service, or server-to-server import from another provider. Name the actual byte destination, authorization mechanism, durable owner, and processing trigger before comparing implementations.
Should files pass through my application server?
Only when the policy or simplicity benefit outweighs owning the data path. Relaying can fit small, infrequent uploads, but direct storage or direct processing avoids consuming application bandwidth, request time, temporary disk, and connection capacity for every byte.
Is a retry the same as a resumable upload?
No. A retry normally starts the transfer again, while resumability continues an existing resource from the byte offset confirmed by the server. The client must retain the upload identity and still handle expiry, changed local files, and ambiguous final responses.
Does Transloadit permanently store uploaded files?
Use an export Robot for files that must persist. Temporary processing results are retained for at least 24 hours, while their URLs can expire after a few hours. Production workflows should export to controlled storage or use temporary URLs only for limited, short-term retrieval into owned infrastructure.
When should I build an upload API instead of buying one?
Building can be sensible for a narrow path with small files, one storage target, predictable networks, and a team ready to own security and operations. Managed infrastructure earns its cost when resumability, remote sources, large files, processing, multiple destinations, or failure recovery would become a separate product.
What belongs in a framework-specific upload guide?
A framework guide should focus on maintainable code, lifecycle behavior, server authorization, recovery, and tests for that framework. Use the linked resumable and API references for exact protocol and request contracts, and evaluate providers against the broader architectural requirements above.
Does Uppy require Transloadit?
No. Uppy is an open-source uploader that also works independently with an S3 bucket, a tus server, or another compatible upload endpoint. Its Transloadit plugin is the integration for managed upload, validation, processing, and export workflows. Choose Uppy with direct storage when transfer is the job; consider Uppy with Transloadit when the uploaded files also need a managed processing workflow.
Can I use a managed file upload API with my own storage?
Yes. In this example, Uppy uploads to Transloadit, the Assembly validates the image and creates a preview, and /s3/store exports the accepted original and preview to your S3 bucket using stored Template Credentials. This is browser-to-Transloadit-to-S3, not a direct browser-to-S3 upload: file bytes and temporary results pass through Transloadit. Your application controls durable storage access, retention, and publication.