AWS S3
The @uppy/aws-s3 plugin uploads files from the browser directly to an S3
bucket, or to any S3-compatible service such as Cloudflare R2, MinIO, or
DigitalOcean Spaces.
Small files are sent in a single request (a presigned PUT in the direct modes,
a POST form upload through Companion), large files as a multipart upload.
Every request to S3 has to be signed, and you choose how that happens:
| Option | Who signs | What you run |
|---|---|---|
getCredentials | the browser, with SigV4 | an endpoint handing out temporary credentials |
signRequest | your own code | an endpoint returning a presigned URL per operation |
companionEndpoint | Companion | Companion |
The three modes are mutually exclusive. Pass exactly one: the plugin throws if you pass none.
Upgrading from Uppy 5.x? All signing options changed names and shapes. See the migration guide.
When should I use it?
Not sure which uploader is best for you? Read “Choosing the uploader you need”.
Use this plugin when you prefer a client-to-storage over a client-to-server-to-storage (such as Transloadit or Tus) setup. This may in some cases be preferable, for instance, to reduce costs or the complexity of running a server and load balancer with Tus.
Multipart uploads become valuable for larger files, as the object is uploaded as
a set of parts. Only failed parts have to be retried, and an interrupted upload
can resume where it stopped instead of starting over. The downside is request
overhead: creation, signing, and completion requests on top of the upload
requests themselves. For a file of a couple of kilobytes with 100 ms
roundtrip latency, that overhead is most of the upload. We recommend keeping the
default shouldUseMultipart, which uses multipart only for large files.
Which signing mode to pick:
signRequestkeeps your credentials on your server and is the safest default.getCredentialsremoves one round trip per S3 operation, because the browser signs locally. In exchange, the browser holds credentials that are valid for whatever your policy grants, so scope that policy tightly.companionEndpointis worth it if you already run Companion for remote sources such as Google Drive.
Install
- NPM
- Yarn
- CDN
npm install @uppy/aws-s3
yarn add @uppy/aws-s3
The bundle consists of most Uppy plugins, so this method is not recommended for production, as your users will have to download all plugins when you are likely using only a few.
It can be useful to speed up your development environment, so don't hesitate to use it to get you started.
<!-- 1. Add CSS to `<head>` -->
<link href="https://releases.transloadit.com/uppy/v5.2.1/uppy.min.css" rel="stylesheet">
<!-- 2. Initialize -->
<div id="uppy"></div>
<script type="module">
import { Uppy, AwsS3 } from "https://releases.transloadit.com/uppy/v5.2.1/uppy.min.mjs"
new Uppy().use(AwsS3, { /* see options */ })
</script>
Use
Whichever mode you pick, your bucket needs CORS rules.
Sign on your server
Your server returns a presigned URL for each S3 operation, and the browser sends the request to that URL. Credentials never leave your server.
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import AwsS3 from '@uppy/aws-s3';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';
new Uppy().use(Dashboard, { inline: true, target: 'body' }).use(AwsS3, {
async signRequest({ method, key, uploadId, partNumber }) {
const response = await fetch('/s3/presign', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ method, key, uploadId, partNumber }),
});
if (!response.ok) throw new Error('Failed to sign S3 request');
// The server responds with `{ "url": "https://…" }`,
// optionally with a `key` if it stored the object elsewhere
return response.json();
},
});
There is no s3Endpoint in this mode: the URL you return determines the host,
bucket, and object key. See signRequest for the
operations your endpoint has to handle, and the
Node.js example
for a working /s3/presign route built on the AWS SDK.
Sign on the client
Your server hands out short-lived credentials (on AWS, from STS), and the browser signs every S3 request itself with SigV4. The credentials are requested once and reused until S3 rejects them as expired.
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import AwsS3 from '@uppy/aws-s3';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';
new Uppy().use(Dashboard, { inline: true, target: 'body' }).use(AwsS3, {
s3Endpoint: 'https://my-bucket.s3.us-east-1.amazonaws.com',
region: 'us-east-1',
async getCredentials() {
const response = await fetch('/s3/sts');
if (!response.ok) throw new Error('Failed to fetch STS credentials');
const { credentials, region } = await response.json();
return {
credentials: {
accessKeyId: credentials.AccessKeyId,
secretAccessKey: credentials.SecretAccessKey,
sessionToken: credentials.SessionToken,
expiration: credentials.Expiration,
},
region,
};
},
});
The credentials are handed to the browser, so the policy you attach to them is the only thing standing between your users and your bucket. Grant the smallest possible set of actions on the smallest possible resource, and keep the lifetime short. Read Requesting temporary security credentials before using this mode.
Use with Companion
Companion has S3 routes built in, and signs the uploads with its own bucket configuration. This is also the mode to use for files coming from remote sources such as Google Drive, which Companion uploads server-side.
Generally it’s better for access control, observability, and scaling to
integrate @uppy/aws-s3 with your own server. You may want to use
Companion for creating, signing, and completing your S3
uploads if you already need Companion for remote files (such as from Google
Drive). Otherwise it’s not worth the hosting effort.
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import AwsS3 from '@uppy/aws-s3';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';
new Uppy().use(Dashboard, { inline: true, target: 'body' }).use(AwsS3, {
companionEndpoint: 'https://companion.uppy.io',
});
Companion decides the bucket, the region, and the object key, so
s3Endpoint, region, and
generateObjectKey are not used in this mode.
API
Options
id
A unique identifier for this plugin (string, default: 'AwsS3').
shouldUseMultipart(file)
A boolean, or a function that returns a boolean which is called for each file
that is uploaded with the corresponding UppyFile instance as argument
(boolean | function, default: files larger than 100 MiB).
uppy.use(AwsS3, {
// …
shouldUseMultipart(file) {
return file.size > 100 * 2 ** 20;
},
});
Files of 5 MiB or smaller are always uploaded in a single request, because S3 does not accept multipart uploads below its minimum part size.
limit
The maximum number of files to upload in parallel (number, default: 6).
The parts of a single file are uploaded one after the other, so this is also the number of concurrent requests to S3. The default matches the number of connections browsers open per host over HTTP/1.1.
getChunkSize(file)
A function that returns the part size in bytes to use when uploading the given
file as multipart (function, default: the file size divided by 10,000). It
receives an object with only a size property, not the full Uppy file.
S3 requires a minimum part size of 5 MiB and supports at most 10,000 parts
per upload. If getChunkSize() returns something smaller, or something that
would produce more than 10,000 parts, the plugin corrects it. A larger part size
means fewer requests, at the cost of re-uploading more data when a part fails.
uppy.use(AwsS3, {
// …
getChunkSize(file) {
return Math.max(5 * 2 ** 20, file.size / 1000);
},
});
generateObjectKey(file)
A function that returns the object key to store the file under (function,
default: `${crypto.randomUUID()}-${file.name}`).
uppy.use(AwsS3, {
// …
generateObjectKey: (file) => `uploads/${Date.now()}-${file.name}`,
});
The key generated here is a proposal. With signRequest,
your server can store the object under a different key by returning it as key.
Not used with companionEndpoint: Companion generates the
key.
signRequest(request)
A function that returns a presigned URL for one S3 operation (function).
Required unless you use getCredentials or
companionEndpoint.
It receives an object with:
method:'PUT' | 'POST' | 'GET' | 'DELETE', the HTTP method of the request.key:string, the object key.uploadId:string, only present for multipart operations.partNumber:number, only present when uploading a part.
It must return a promise for an object with:
url:string, the presigned URL.key:string, optional. The key your server stored the object under. Uppy uses it for the rest of the upload and reports it inupload-success. Leave it out if the server used the key Uppy sent.
These are the operations your endpoint has to sign:
| Request | S3 operation |
|---|---|
{ method: 'PUT', key } | PutObject |
{ method: 'POST', key } | CreateMultipartUpload |
{ method: 'PUT', key, uploadId, partNumber } | UploadPart |
{ method: 'GET', key, uploadId } | ListParts |
{ method: 'POST', key, uploadId } | CompleteMultipartUpload |
{ method: 'DELETE', key, uploadId } | AbortMultipartUpload |
If shouldUseMultipart is false for every file,
the first row is the only one you need.
To generate the object key on your server instead, change it only on the request
that creates the object: the single-part PUT, or the POST without an
uploadId. Return the key you used as key there. Every request that carries
an uploadId must be signed for exactly the key it arrives with. That key was
fixed when the upload was created, and a key returned on those requests is
ignored. A server that derives the key again on every request prefixes a key
that is already prefixed, and S3 answers with NoSuchUpload. The
Node.js
and
PHP
signer examples show how to do this correctly.
getCredentials()
A function that returns temporary security credentials for client-side signing
(function). Required, together with s3Endpoint, unless you
use signRequest or
companionEndpoint.
It must return an object (or a promise for one) with:
credentials:objectaccessKeyId:stringsecretAccessKey:stringsessionToken:stringexpiration:string, optional ISO 8601 date.
region:string, the region to sign for. Takes precedence overregion.
The result is cached for the lifetime of the plugin, so your endpoint is hit
once instead of once per request. When S3 answers with ExpiredToken or
InvalidAccessKeyId, the plugin drops the cache, calls getCredentials()
again, and retries the request once.
AWS STS returns AccessKeyId, SecretAccessKey, and SessionToken in
PascalCase; map them to the camelCase keys above, as in the
example.
s3Endpoint
The URL of the bucket to upload to (string). Required with
getCredentials, unused in the other modes.
Both the virtual-hosted style (https://my-bucket.s3.us-east-1.amazonaws.com)
and the path style (https://s3.us-east-1.amazonaws.com/my-bucket) work. The
object key is appended to it.
region
The region to sign requests for (string, default: 'auto'). Used with
getCredentials, and overridden by the region that
getCredentials() returns.
'auto' is what S3-compatible services that have no regions, such as Cloudflare
R2, expect. AWS S3 needs the real region of the bucket.
companionEndpoint
The URL of your Companion instance (string). Required unless
you use signRequest or
getCredentials.
allowedMetaFields
Pass an array of field names to limit the metadata fields that are sent to
Companion, which stores them as S3 object metadata (boolean | Array, default:
true).
- Set it to
falseto not send any fields (or an empty array). - Set it to
['name']to only send thenamefield. - Set it to
true(the default) to send all metadata fields.
Object metadata is only sent in the Companion mode. The two direct signing modes upload the file contents and nothing else.
S3-compatible services
Any service that speaks the S3 API works in every signing mode. For
signRequest there is nothing to configure, as your
signer decides the host. For getCredentials, point
s3Endpoint at the service and set the region it
expects.
Cloudflare R2:
uppy.use(AwsS3, {
s3Endpoint: 'https://<account-id>.r2.cloudflarestorage.com/my-bucket',
region: 'auto',
getCredentials,
});
MinIO, or another self-hosted service:
uppy.use(AwsS3, {
s3Endpoint: 'https://minio.my-app.com/my-bucket',
region: 'us-east-1',
getCredentials,
});
Bucket and CORS setup
S3 buckets do not allow public uploads for security reasons. To allow Uppy and the browser to upload directly to a bucket, its CORS permissions need to be configured.
CORS permissions can be found in the
S3 Management Console. Click the
bucket that will receive the uploads, then go into the Permissions tab and
select the CORS configuration button. A JSON document will be shown that
defines the CORS configuration. (AWS used to use XML but now only allow JSON).
More information about the
S3 CORS format here.
The configuration required for Uppy is this:
[
{
"AllowedOrigins": ["https://my-app.com"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"MaxAgeSeconds": 3000,
"AllowedHeaders": ["content-type"],
"ExposeHeaders": ["ETag", "Location"]
},
{
"AllowedOrigins": ["*"],
"AllowedMethods": ["GET"],
"MaxAgeSeconds": 3000
}
]
A good practice is to use two CORS rules: one for uploading files (the first object in the array) and one for viewing them from the browser (the second). CORS only controls which origins the browser lets read the responses. Whether an object can be viewed at all is decided by your bucket policy and ACLs, not by this rule.
ExposeHeaders has to include ETag, or multipart uploads cannot be completed:
the browser needs to read the ETag of every part it uploads. DELETE is used
to abort a multipart upload when the user cancels one, and GET to list the
parts of an upload that is being resumed.
If you are using an IAM policy to allow access to the S3 bucket, the policy
needs at least the s3:PutObject, s3:ListMultipartUploadParts (listing parts
when an upload resumes), and s3:AbortMultipartUpload (cancelling an upload)
permissions scoped to the bucket in question. Add s3:PutObjectAcl only if you
configure an ACL on uploads. In-depth documentation about CORS rules is
available on the
AWS documentation site.
Use with TypeScript
Uppy always puts the response to an upload in file.response.body. If you want
this to be type safe with @uppy/aws-s3, you can import the AwsBody type and
pass it as the second generic to Uppy.
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import AwsS3, { type AwsBody } from '@uppy/aws-s3';
import '@uppy/core/css/style.min.css';
import '@uppy/dashboard/css/style.min.css';
// Set this to `any` or `Record<string, unknown>`
// if you do not set any metadata yourself
type Meta = { license: string };
const uppy = new Uppy<Meta, AwsBody>()
.use(Dashboard, { inline: true, target: 'body' })
.use(AwsS3, { companionEndpoint: 'https://companion.uppy.io' });
const id = uppy.addFile(/* … */);
await uppy.upload();
const body = uppy.getFile(id).response.body!;
const { location, key } = body; // This is now type safe