Uppy 6.0 : fewer packages, fewer moving parts, and a robust S3 Plugin
Uppy 6.0 is here, freshly groomed and ready to play. Rather than teaching it new tricks, we spent this release mostly cleaning up after our faithful file-uploading companion. Every pup needs a bath eventually.
We rewrote @uppy/aws-s3 from scratch, folded four packages into @uppy/core,
and dropped a fifth entirely.
If you have ever debugged a duplicate-version bug in your lockfile or given up halfway through configuring an S3 upload, this release is for you.
Migration guide
This post covers the highlights. Six packages take a major bump: @uppy/core,
@uppy/companion, @uppy/aws-s3, @uppy/tus, @uppy/components, and uppy.
If you use the uppy meta-package or the CDN bundle, most of this release
requires no changes on your side.
We have an accompanying migration guide for everyone else.
@uppy/aws-s3, rewritten from scratch
Configuring the old plugin meant implementing up to eight callbacks:
getUploadParameters, getTemporarySecurityCredentials, uploadPartBytes,
signPart, createMultipartUpload, listParts, abortMultipartUpload, and
completeMultipartUpload. Which ones you needed depended on whether you were
doing multipart, whether Companion was in the picture, and whether you were
signing on the client. Nothing told you that up front. You read all eight and
guessed.
We received many reports from people who had wired up a plausible-looking combination that quietly did not work. Over time, it became clear that the problem was the shape of the API rather than any individual bug in it. So we started over.
There are now three signing modes, and you pick exactly one:
import Uppy from '@uppy/core';
import AwsS3 from '@uppy/aws-s3';
new Uppy().use(AwsS3, {
s3Endpoint: 'https://my-bucket.s3.eu-west-1.amazonaws.com',
region: 'eu-west-1',
getCredentials: async () => fetch('/s3-credentials').then((r) => r.json()),
});
The example above uses getCredentials, which signs on the client using SigV4.
The other two are signRequest, where you bring your own signer, and
companionEndpoint, where Companion signs for you.
Companion is no longer required in the data path, and neither is AWS.
The plugin now talks to any S3-compatible service, so Cloudflare R2, MinIO and DigitalOcean Spaces work, without the need for any provider-specific code.
The rewrite also closed a significant number of reliability bugs that had been open for a while: retries with backoff, credential expiry, offline detection, and pause/resume races during multipart uploads. The plugin is now tested against a real MinIO instance in CI instead of mocks, which is how several of those bugs were caught in the first place.
Read the full rewrite deep-dive →
One core: a single source of truth
We published @uppy/utils, @uppy/store-default, @uppy/companion-client and
@uppy/provider-views as separate packages, because that is what a monorepo
invites you to do. They are no longer published separately. Their code ships
inside @uppy/core as subpath exports:
- import { fetcher } from '@uppy/utils'
+ import { fetcher } from '@uppy/core/utils'
- import { RequestClient } from '@uppy/companion-client'
+ import { RequestClient } from '@uppy/core/companion-client'
This was not housekeeping. Those four packages sat behind every plugin as
sub-dependencies, which meant a lockfile could, and regularly did, pin an old
copy of one of them while @uppy/core moved on. While users simply saw “Uppy is
broken”, in actuality there were two versions of @uppy/utils in the same tree.
Every plugin already depends on @uppy/core, so there is now only one source of
truth. And as a result, that entire category of bugs is gone.
Removing the package boundary bought us something else as well: co-dependent
types can finally reference each other directly. CompanionClientProvider and
CompanionClientSearchProvider were hand-maintained stand-ins that only existed
because @uppy/utils could not see the real provider classes. Keeping them in
sync was a chore nobody enjoyed. Both are removed. Import Provider from
@uppy/core/companion-client instead.
Transloadit: build your own assembly UI
@uppy/transloadit now puts assembly status in plugin state, so you can build
your own progress UI instead of styling ours:
import { useUppyState } from '@uppy/react';
function AssemblyProgress({ uppy }) {
const { assemblyStatus, lastAssemblyStatus } = useUppyState(
uppy,
(state) => state.plugins.Transloadit,
);
const status = assemblyStatus ?? lastAssemblyStatus;
if (!status) return null;
return <p>{status.ok}</p>;
}
assemblyStatus follows the live assembly through every transition and clears
when there is no active assembly. lastAssemblyStatus keeps the previous run’s
result around, so your UI has something to show between uploads instead of
flickering to empty. That second field exists because the first one on its own
made for a UI that kept blanking out, which we only noticed once we tried
building something with it.
Recovery got better alongside it. @uppy/golden-retriever now stores file
metadata in IndexedDB and falls back to localStorage where IndexedDB is not
available. The old localStorage-only store ran into quota limits on large
assemblies, so the uploads most worth recovering were the ones that could not be
recovered. Large assemblies now restore.
Companion
OAuth tokens now travel over a WebSocket
Companion used to hand the token back through window.opener. It now sends it
over the WebSocket connection instead. This breaks in both directions:
@uppy/core needs the newest Companion, and companion.socket() now takes
companionOptions as its second argument. If you self-host, upgrade both
sides together. If you use hosted Companion, you do not need to do anything.
Express 5
Companion runs on Express 5. If you mount it as middleware inside an Express 4 app, it will no longer work. You will need to upgrade your app first.
TypeScript
Companion is written in TypeScript as of this release. On paper, that changes nothing for you. Ports of this size tend to shake things loose, though, so please report anything that looks off.
Supply-chain hardening
After a rough year for the npm ecosystem, we tightened up how dependencies enter this repository. Packages must be at least seven days old before Yarn will resolve them. Dependabot updates carry a matching cooldown, and every third-party GitHub Action is pinned to a commit SHA. Security updates deliberately bypass the cooldown, so fixes still land immediately.
None of this changes the API you write against. It changes what can end up in a release, which felt worth the trouble.
Deprecations and removals
@uppy/instagramis removed. In 2024 Instagram disabled their API and the plugin stopped working, so we have now finally removed it.@uppy/utils,@uppy/store-default,@uppy/companion-clientand@uppy/provider-viewsare no longer published as standalone packages. Their existing releases stay on npm but are deprecated. See One core.- Provider CSS moved to
@uppy/core/provider-views/css/style.min.css. Most apps never imported this directly, since it ships bundled in@uppy/dashboard’s CSS.
And more
@uppy/tus no longer aborts the request when it errors, so the server’s status
and body now reach upload-error and file.response instead of arriving as
status 0. If you have error-handling code written against the old behavior,
give it a look.
Beyond that: Angular 22 support in @uppy/angular, the Dashboard’s “My Device”
button now respects fileManagerSelectionType, and locale updates including
Norwegian Bokmål.
This release contains 58 pull requests. Most of them are too small to mention here, but they all add up to a version we are happy to put our name on.
Ready to upgrade? Start with the migration guide, and open an issue if something breaks.


