Skip to main content

Uppy 6.0 : fewer packages, fewer moving parts, and a robust S3 Plugin

· 7 min read

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/instagram is 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-client and @uppy/provider-views are 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.