Purpose: This document serves as the comprehensive reference for building fluppy, a Flutter package that aims to be 1:1 similar to Uppy.js. All architectural decisions, API designs, and feature implementations should align with the patterns documented here.
Last Updated: 2026-01-11 Uppy Version Referenced: Latest (as of Jan 2026) Official Documentation: https://uppy.io/docs/
- Overview & Philosophy
- Core Architecture
- State Management
- File Object Model
- Event System
- Upload Lifecycle & Pipeline
- Plugin System
- Uploader Implementations
- Configuration & Options
- API Reference
- Fluppy Mapping Strategy
Uppy is a sleek, modular JavaScript file uploader that integrates seamlessly with any application. It's:
- Fast: Optimized for performance
- Modular: Plugin-based architecture
- Lightweight: Light on dependencies
- Headless: No UI required (but UI plugins available)
- Resumable: Built-in support for resumable uploads via tus protocol
- Flexible: Supports local files, remote sources (Google Drive, Dropbox, etc.)
- Modularity First: Core orchestrator + plugin ecosystem
- Backend-Agnostic: Works with any upload backend
- User-Centric: Comprehensive progress tracking and error handling
- Resumability: Network interruptions shouldn't restart uploads
- Extensibility: Easy to add custom functionality via plugins
- Separation of Concerns: State management, UI, and upload logic are separate
Uppy Core (@uppy/core) functions as:
- State Manager: Immutable state store with Redux-like patterns
- Event Emitter: Pub/sub system for lifecycle events
- Restrictions Handler: File validation and constraints enforcement
- Plugin Manager: Install/uninstall/lifecycle management
- Upload Coordinator: Controls upload pipeline execution
┌─────────────────────────────────────────────┐
│ Uppy Core (Orchestrator) │
│ ┌────────────┐ ┌──────────┐ ┌─────────┐ │
│ │ State │ │ Events │ │ Plugins │ │
│ │ Manager │ │ Emitter │ │ Manager │ │
│ └────────────┘ └──────────┘ └─────────┘ │
└─────────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ UI Plugins │ │ Uploader │
│ (Optional) │ │ Plugins │
└────────────┘ └────────────┘
Features are added incrementally through plugins:
Plugin Categories:
- UI Plugins: Dashboard, Drag & Drop, File Input, Webcam
- Upload Plugins: Tus, XHR Upload, AWS S3, Transloadit
- Processing Plugins: Image Editor, Thumbnail Generator, Golden Retriever (state recovery)
- Remote Source Plugins: Google Drive, Dropbox, Instagram, Box
Key Insight: Uppy Core can function without any UI plugins—developers can build completely custom interfaces.
Uppy maintains an immutable state object:
{
plugins: {}, // Plugin registry
files: {}, // File objects keyed by ID
currentUploads: {}, // Active upload tracking
capabilities: { // Feature detection
resumableUploads: false
},
totalProgress: 0, // 0-100 aggregate progress
meta: {}, // Global metadata
info: { // Notification state
isHidden: true,
type: 'info',
message: ''
}
}Immutability Pattern (Redux-style):
- State mutations create copies, never modify directly
- Use spread operators or Object.assign
- Enables time-travel debugging and predictable behavior
State Access Methods:
getState(): Retrieve current state snapshotsetState(patch): Merge patch into state (shallow merge)setFileState(fileID, state): Update individual file state
Uppy supports custom stores (e.g., Redux integration):
- Provide store with
getState()andsetState()methods - Uppy will use your store instead of internal one
- Enables unified state management across app
Every file in Uppy is a wrapped object:
{
id: 'uppy-generated-unique-id',
name: 'vacation.jpg',
extension: 'jpg',
type: 'image/jpeg',
size: 2097152, // bytes
data: File, // Browser File/Blob (local only)
source: 'Dashboard', // Plugin name that added this file
isRemote: false, // Remote provider file?
meta: { // Extensible metadata
name: 'vacation.jpg',
type: 'image/jpeg',
relativePath: 'photos/vacation.jpg',
absolutePath: '/Drive/photos/vacation.jpg'
},
progress: { // Upload tracking
bytesUploaded: 0,
bytesTotal: 2097152,
uploadStarted: 1640000000000,
uploadComplete: false,
percentage: 0
},
preview: 'data:image/jpeg;base64,...', // Optional thumbnail
uploadURL: 'https://cdn.com/file.jpg', // Post-upload URL
remote: {}, // Provider metadata
response: { // Server response
status: 200,
body: {},
uploadURL: 'https://...'
}
}Uppy supports multiple file sources:
- Local Files: Standard browser File objects
- Remote Files: From Google Drive, Dropbox, etc.
- Webcam: Live capture
- URL Import: Fetch from external URLs
- Clipboard: Paste from clipboard
Important: file.data only exists for local files. Remote files use file.remote metadata.
Local Files:
relativePath: Path relative to dropped folder (e.g., "folder1/folder2/file.jpg")
Remote Files:
absolutePath: Full path from provider root- Reflects user's navigation depth in provider
Uppy emits granular events for every lifecycle stage:
uppy.on('event-name', (data) => {
// Handle event
})| Event | When Fired | Payload |
|---|---|---|
file-added |
Single file added | (file) |
files-added |
Multiple files added | (files[]) |
file-removed |
File deleted | (file, reason) |
| Event | When Fired | Payload |
|---|---|---|
upload |
Upload initiated | (data: { id, fileIDs }) |
upload-start |
Single file starts | (file) |
upload-progress |
Progress update | (file, progress) |
upload-success |
File completed | (file, response) |
upload-error |
Upload failed | (file, error, response) |
upload-retry |
Retry initiated | (fileID) |
retry-all |
All retried | (fileIDs) |
upload-pause |
Upload paused | (fileID, isPaused) |
upload-stalled |
No progress timeout | () |
complete |
All finished | (result) |
cancel-all |
All cancelled | () |
| Event | When Fired | Payload |
|---|---|---|
preprocess-progress |
Pre-processing | (file, progress) |
postprocess-progress |
Post-processing | (file, progress) |
preprocess-complete |
Batch pre-processed | (fileID) |
postprocess-complete |
Batch post-processed | (fileID) |
| Event | When Fired | Payload |
|---|---|---|
progress |
Total progress update | (progress: 0-100) |
| Event | When Fired | Payload |
|---|---|---|
info-visible |
Info message shown | () |
info-hidden |
Info message hidden | () |
restriction-failed |
Validation failed | (file, error) |
| Event | When Fired | Payload |
|---|---|---|
error |
Critical error | (error) |
reset |
State cleared | () |
Determinate Progress (with known total):
uppy.emit('preprocess-progress', file, {
mode: 'determinate',
message: 'Processing...',
value: 50 // 0-100
})Indeterminate Progress (unknown duration):
uppy.emit('preprocess-progress', file, {
mode: 'indeterminate',
message: 'Analyzing...'
})Uppy processes files through three distinct phases:
┌──────────────┐ ┌──────────┐ ┌───────────────┐
│ Preprocessing│ -> │ Uploading│ -> │ Postprocessing│
└──────────────┘ └──────────┘ └───────────────┘
Purpose: Prepare files before upload Use Cases:
- Image resizing/compression
- File encryption
- Metadata extraction
- Format conversion
- Validation
API:
uppy.addPreProcessor((fileIDs) => {
return new Promise((resolve, reject) => {
// Process files
// Use uppy.setFileState() to modify files
resolve()
})
})Purpose: Transmit files to destination Use Cases:
- HTTP upload
- Resumable upload (tus)
- Cloud storage (S3, GCS)
- Custom protocols
API:
uppy.addUploader((fileIDs) => {
return new Promise((resolve, reject) => {
// Upload files
// Emit upload-progress events
resolve()
})
})Purpose: Actions after successful upload Use Cases:
- CDN propagation waiting
- Thumbnail generation (server-side)
- Database record creation
- Webhook notifications
- Encoding completion
API:
uppy.addPostProcessor((fileIDs) => {
return new Promise((resolve, reject) => {
// Post-process
resolve()
})
})Removal:
const fn = (fileIDs) => { /* ... */ }
uppy.addPreProcessor(fn)
uppy.removePreProcessor(fn) // Must be same function referenceCritical: Bind functions before adding to enable removal:
this.myProcessor = this.myProcessor.bind(this)
uppy.addPreProcessor(this.myProcessor)For plugins without user interface:
class MyPlugin extends BasePlugin {
constructor(uppy, opts) {
super(uppy, opts)
this.id = opts.id || 'MyPlugin'
this.type = 'uploader' // or 'acquirer', 'modifier', etc.
}
install() {
// Setup: attach event listeners
}
uninstall() {
// Cleanup: remove listeners, cancel operations
}
afterUpdate() {
// Called after state changes (debounced)
}
}For plugins that render interface:
class MyUIPlugin extends UIPlugin {
constructor(uppy, opts) {
super(uppy, opts)
this.id = opts.id || 'MyUIPlugin'
this.type = 'acquirer'
}
render() {
// Return Preact elements
return <div>My Plugin UI</div>
}
mount(target, plugin) {
// Attach to DOM or parent plugin
}
onMount() {
// After render completes
}
onUnmount() {
// Before removal
}
}- Registration:
uppy.use(MyPlugin, options) - Installation:
install()method called - Mounting (UI plugins):
mount()called - Rendering (UI plugins):
render()called on state changes - Updates:
afterUpdate()called after state mutations - Uninstallation:
uninstall()method called - Removal:
uppy.removePlugin(instance)
Uppy recognizes these plugin types (used by Dashboard for layout):
acquirer: File source plugins (local, webcam, remote)uploader: Upload destination pluginsprogressindicator: Progress display pluginseditor: File editing pluginspresenter: Dashboard-like container pluginsmodifier: File transformation plugins
Global State Access:
const allFiles = this.uppy.getState().filesPlugin-Specific State:
this.setPluginState({ isOpen: true })
const pluginState = this.getPluginState()File State Modification:
this.uppy.setFileState(fileID, {
name: 'new-name.jpg',
meta: { ...file.meta, customField: 'value' }
})Define strings with plural support:
this.defaultLocale = {
strings: {
youCanOnlyUploadX: {
0: 'You can only upload %{smart_count} file',
1: 'You can only upload %{smart_count} files'
}
}
}
this.i18nInit() // Call in constructorOverride via locale option:
uppy.use(MyPlugin, {
locale: {
strings: {
youCanOnlyUploadX: 'Maximum %{smart_count} files'
}
}
})Protocol: Open standard for resumable uploads over HTTP
Package: @uppy/tus
Key Features:
- Automatic resume after network failures
- HTTP PATCH-based upload
- Exponential backoff on errors
- Chunk-based transmission
Configuration:
uppy.use(Tus, {
endpoint: 'https://tusd.tusdemo.net/files/',
chunkSize: 5 * 1024 * 1024, // 5 MiB chunks
retryDelays: [0, 1000, 3000, 5000],
headers: {},
limit: 20, // Concurrent uploads
withCredentials: false,
allowedMetaFields: null // All fields
})Resume Mechanism:
- Server assigns unique upload ID
- Client stores ID + offset
- On reconnect, client queries offset via HEAD request
- Upload continues from last received byte
Package: @uppy/aws-s3
Upload Modes: Single-part, Multipart
Architecture: Client-to-storage (direct uploads)
For files ≤ 100 MiB (default threshold):
uppy.use(AwsS3, {
getUploadParameters(file) {
return fetch('/s3/params', {
method: 'POST',
body: JSON.stringify({ filename: file.name })
})
.then(res => res.json())
.then(data => ({
method: 'PUT',
url: data.presignedUrl,
headers: {
'Content-Type': file.type
}
}))
}
})For files > 100 MiB:
Flow:
createMultipartUpload()→ Get uploadId- Split file into chunks (5 MiB minimum)
signPart()→ Get presigned URL for each part- Upload parts in parallel (configurable concurrency)
listParts()→ Verify uploaded partscompleteMultipartUpload()→ Finalize
Configuration:
uppy.use(AwsS3, {
createMultipartUpload(file) {
// Create multipart upload
return { uploadId, key }
},
signPart(file, partData) {
// Sign individual part
return { url, headers }
},
completeMultipartUpload(file, { uploadId, key, parts }) {
// Complete upload
return { location }
},
abortMultipartUpload(file, { uploadId, key }) {
// Cleanup on failure
},
listParts(file, { uploadId, key }) {
// Get already uploaded parts (for resume)
return parts[]
},
getChunkSize(file) {
return 5 * 1024 * 1024 // 5 MiB
},
shouldUseMultipart(file) {
return file.size > 100 * 1024 * 1024 // 100 MiB
},
limit: 6, // Parallel files
retryDelays: [0, 1000, 3000, 5000]
})Temporary Credentials Mode:
uppy.use(AwsS3, {
getTemporarySecurityCredentials() {
return fetch('/s3/credentials')
.then(res => res.json())
.then(data => ({
accessKeyId: data.AccessKeyId,
secretAccessKey: data.SecretAccessKey,
sessionToken: data.SessionToken,
region: 'us-east-1',
bucket: 'my-bucket'
}))
}
})Benefits: ~20% faster (no backend signing), reduced server load
Package: @uppy/xhr-upload
Use Case: Traditional HTTP form uploads
Protocol: Standard XMLHttpRequest
Configuration:
uppy.use(XHRUpload, {
endpoint: 'https://api.example.com/upload',
method: 'POST',
formData: true,
fieldName: 'files[]',
headers: {},
bundle: false, // Upload individually
limit: 5,
timeout: 30 * 1000,
withCredentials: false,
allowedMetaFields: null,
getResponseData(responseText, response) {
return { url: JSON.parse(responseText).url }
},
onBeforeRequest(xhr, { file }) {
// Modify request before sending
},
onAfterResponse(xhr, { file }) {
// Handle response
}
})Bundle Mode:
uppy.use(XHRUpload, {
bundle: true, // Single request for all files
formData: true
})| Option | Type | Default | Description |
|---|---|---|---|
id |
string | 'uppy' |
Unique instance identifier |
autoProceed |
boolean | false |
Auto-upload on file add |
allowMultipleUploadBatches |
boolean | true |
Allow sequential uploads |
debug |
boolean | false |
Enable debug logging |
logger |
object | justErrorsLogger |
Custom logger |
{
restrictions: {
maxFileSize: 1024 * 1024 * 100, // 100 MiB per file
minFileSize: null,
maxTotalFileSize: null, // All files combined
maxNumberOfFiles: 10,
minNumberOfFiles: null,
allowedFileTypes: ['image/*', '.jpg', '.jpeg', '.png'],
requiredMetaFields: ['name'], // Enforce metadata
}
}File Type Formats:
- MIME type:
'image/jpeg' - MIME wildcard:
'image/*' - Extension:
'.jpg'
{
meta: {
projectId: '12345',
userId: 'abc',
customField: 'value'
}
}Merged with each file's metadata.
{
onBeforeFileAdded(currentFile, files) {
// Validate/modify before adding
// Return false to reject
// Return modified file to change
if (currentFile.size > 1000000) {
return false
}
return {
...currentFile,
name: 'prefixed-' + currentFile.name
}
},
onBeforeUpload(files) {
// Final check before upload
// Return false to prevent upload
if (Object.keys(files).length < 2) {
return false
}
}
}{
infoTimeout: 5000, // Milliseconds to show notifications
}{
store: myReduxStore // Custom state management
}Add single file to state.
Parameters:
uppy.addFile({
name: 'photo.jpg',
type: 'image/jpeg',
data: fileBlob,
source: 'Local',
isRemote: false,
meta: {
customField: 'value'
}
})Returns: fileID (string)
Add multiple files.
Returns: void
Remove file and cancel ongoing uploads.
Retrieve file object.
Returns: File object or undefined
Get all files.
Returns: Array of file objects
Remove all files and reset state.
Start upload for all added files.
Returns: Promise
uppy.upload()
.then((result) => {
console.log('Successful uploads:', result.successful)
console.log('Failed uploads:', result.failed)
})Toggle pause/resume for specific file.
Pause all uploads.
Resume all paused uploads.
Retry failed upload.
Retry all failed uploads.
Cancel uploads and remove all files.
Merge patch into state.
uppy.setState({
meta: { userId: '123' }
})Get current state snapshot.
Update individual file state.
uppy.setFileState(fileID, {
name: 'new-name.jpg',
meta: { ...file.meta, edited: true }
})Update global metadata.
uppy.setMeta({ projectId: '456' })Update file-specific metadata.
uppy.setFileMeta(fileID, { caption: 'Sunset photo' })Install plugin.
uppy.use(Dashboard, {
target: '#uppy',
inline: true
})Uninstall plugin.
const dashboard = uppy.getPlugin('Dashboard')
uppy.removePlugin(dashboard)Retrieve plugin instance by ID.
Show notification.
uppy.info('Upload complete!', 'success', 3000)Types: 'info', 'warning', 'error', 'success'
Log message.
Types: 'debug', 'warning', 'error'
Call logout on all remote provider plugins.
Cleanup: uninstall plugins, remove listeners, reset state.
| Uppy Concept | Fluppy Implementation | Status |
|---|---|---|
| Uppy Core | Fluppy class |
✅ Implemented |
| BasePlugin | Uploader abstract class |
✅ Implemented |
| UIPlugin | Not needed (Flutter has widgets) | N/A |
| Tus Uploader | TusUploader |
❌ Not implemented |
| S3 Uploader | S3Uploader |
✅ Implemented |
| XHR Upload | HttpUploader |
❌ Not implemented |
| Event System | Sealed class events + Stream | ✅ Implemented |
| State Management | Internal state + getters | ✅ Implemented |
| Uppy API | Fluppy API | Notes |
|---|---|---|
addFile(obj) |
addFile(FluppyFile) |
✅ Implemented |
addFiles(arr) |
addFiles(List<FluppyFile>) |
✅ Implemented |
removeFile(id) |
removeFile(String id) |
✅ Implemented |
getFile(id) |
getFile(String id) |
✅ Implemented |
getFiles() |
files getter |
✅ Implemented |
upload() |
upload() |
✅ Implemented |
pauseAll() |
pauseAll() |
✅ Implemented |
resumeAll() |
resumeAll() |
✅ Implemented |
cancelAll() |
cancelAll() |
✅ Implemented |
retryAll() |
retryAll() |
✅ Implemented |
setState() |
N/A (internal) | State management is internal |
on() |
events stream |
✅ Stream-based |
destroy() |
dispose() |
✅ Implemented |
| Uppy Event | Fluppy Event | Class |
|---|---|---|
file-added |
FileAdded |
✅ |
file-removed |
FileRemoved |
✅ |
upload |
UploadStarted |
✅ |
upload-progress |
UploadProgress |
✅ |
upload-success |
UploadComplete |
✅ |
upload-error |
UploadError |
✅ |
upload-pause |
UploadPaused / UploadResumed |
✅ |
upload-retry |
UploadRetry |
✅ |
complete |
AllUploadsComplete |
✅ |
cancel-all |
UploadCancelled |
✅ |
preprocess-progress |
❌ Not implemented | Need preprocessing |
postprocess-progress |
❌ Not implemented | Need postprocessing |
- File management (add, remove, batch operations)
- Event-driven architecture
- Progress tracking
- Pause/resume/cancel
- Retry logic with delays
- Concurrent upload limits
- File metadata support
- Lifecycle management (dispose)
- S3 single-part upload
- S3 multipart upload
- Presigned URL support
- Temporary credentials (STS)
- AWS Signature V4
- Chunk upload
- Resume capability (list parts)
- Abort multipart
- Tus protocol
- XHR/HTTP upload
- GCS upload
- Azure Blob Storage
- Preprocessing pipeline
- Postprocessing pipeline
- Plugin system (extensible architecture)
- Multiple uploader plugins
- Remote file sources (Google Drive, Dropbox)
- Webcam capture
- URL import
- File restrictions validation
- i18n support
- Custom state store
- Golden Retriever (state recovery)
- Dart (server/CLI support)
- Flutter (mobile/web UI)
- Web platform support (dart:html bridge)
- Cross-platform file picker
- Platform-specific optimizations
-
Add Preprocessing/Postprocessing Pipeline
- Essential for image compression, validation, post-upload workflows
- Core to Uppy's architecture
-
Implement Tus Uploader
- Most important feature after S3
- Universal resumable upload standard
-
Add File Restrictions
maxFileSize,minFileSize,allowedFileTypes, etc.- Critical for validation
-
Implement XHR/HTTP Uploader
- Basic uploader for simple backends
- Most common use case after S3
-
Plugin System Refactor
- Make uploader registration more flexible
- Allow multiple uploaders in one instance
- Support uploader selection per file
-
Flutter Integration
- File picker integration
- UI components (optional)
- Platform-specific handling
-
State Recovery (Golden Retriever)
- Save state to persistent storage
- Resume uploads after app restart
-
More Tests
- S3Uploader integration tests
- AWS Signature V4 tests
- End-to-end scenarios
-
Remote Sources
- Google Drive, Dropbox plugins
- Requires Companion server
-
UI Components
- Flutter Dashboard widget
- Progress indicators
- File list views
Keep Similar to Uppy:
- Core class:
Uppy→Fluppy✅ - Methods:
addFile,removeFile,upload, etc. ✅ - Events:
file-added→FileAdded, etc. ✅ - Options:
retryDelays,limit, etc. ✅
Dart Conventions:
- Use
UpperCamelCasefor classes - Use
lowerCamelCasefor methods/properties - Use sealed classes for events (type-safe sum types)
- Use streams instead of EventEmitter
Fluppy has established a solid foundation with:
- Core orchestrator pattern
- Event-driven architecture
- Complete S3 support (single + multipart)
- Progress tracking and lifecycle management
Next steps for 1:1 Uppy parity:
- Add preprocessing/postprocessing pipelines
- Implement Tus uploader
- Add file restrictions
- Build HTTP/XHR uploader
- Expand plugin system
This document should serve as the single source of truth for architectural decisions and feature implementation.