Pick S3, R2, Tigris or Cloudinary, type your bucket and site origins, and copy the .env template, CORS rules and permissions UploadCN needs.
Direct-to-bucket uploads need three things outside your code: environment variables for the upload route, a CORS rule on the bucket so browsers may upload to it, and an access key that is allowed to do it. Each provider wants them in its own format. Pick yours, fill in the bucket name and the origins your users upload from, and copy each block.
The sites your users upload from, one per line or separated by commas. Scheme and host only, no path.
Environment variables
# Amazon S3. Read by the upload route: npx shadcn@latest add @uploadcn/upload-route
S3_BUCKET=my-bucket
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
# Optional: a public base URL for reading files (a CDN or public bucket)
# S3_PUBLIC_URL=- Leave S3_ENDPOINT unset for AWS: it is only for S3-compatible services.
- The scaffolded route uses R2 when R2_ACCOUNT_ID is set, so keep the R2_* variables out of this file.
CORS
The browser uploads straight to the bucket, so the bucket has to allow your origins. Use either format.
1. S3 console
Open the bucket, then Permissions, then Cross-origin resource sharing (CORS), and paste this JSON.
[
{
"AllowedOrigins": [
"https://your-app.com"
],
"AllowedMethods": [
"PUT"
],
"AllowedHeaders": [
"content-type"
],
"ExposeHeaders": [
"ETag"
],
"MaxAgeSeconds": 3600
}
]2. AWS CLI
The CLI wraps the same rules in a CORSRules object. Save it as cors.json, then run the command.
{
"CORSRules": [
{
"AllowedOrigins": [
"https://your-app.com"
],
"AllowedMethods": [
"PUT"
],
"AllowedHeaders": [
"content-type"
],
"ExposeHeaders": [
"ETag"
],
"MaxAgeSeconds": 3600
}
]
}aws s3api put-bucket-cors --bucket my-bucket --cors-configuration file://cors.jsonIAM policy
The least the access key behind S3_ACCESS_KEY_ID needs. Attach it to the IAM user or role that owns the key.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}- s3:PutObject also covers creating, uploading the parts of and completing multipart uploads.
- The bucket can stay private: uploads use signed URLs, so no bucket policy or public access is needed.
Abandoned multipart uploads
Optional. Parts of uploads that are never finished are billed until removed. This lifecycle rule aborts them after 7 days.
{
"Rules": [
{
"ID": "abort-incomplete-multipart-uploads",
"Status": "Enabled",
"Filter": {
"Prefix": ""
},
"AbortIncompleteMultipartUpload": {
"DaysAfterInitiation": 7
}
}
]
}aws s3api put-bucket-lifecycle-configuration --bucket my-bucket --lifecycle-configuration file://lifecycle.json- put-bucket-lifecycle-configuration replaces the bucket's whole lifecycle configuration. If the bucket already has rules, add this one to them instead.
Official Amazon S3 docs
These formats were checked against the pages below. Providers change their consoles and APIs, so follow the links when something does not match. The full UploadCN setup is in the Amazon S3 guide.
Everything is generated in your browser as you type. Nothing you enter is sent anywhere,
and the output never contains secrets: paste your keys into .env.local yourself.
The tool works with a keyboard and a screen reader: the providers are a radio group, every code block can be focused and scrolled, each copy is confirmed in words, and problems with what you typed are announced as you type.
What the output is for
| Block | Where it goes |
|---|---|
.env.local | Your app. The names are the ones the upload route reads. |
| CORS | The bucket, through the provider's console or CLI. |
| IAM policy or token | The access key your server signs uploads with. |
| Upload preset | Cloudinary only: the unsigned preset the browser uploads with. |
Why these rules
Every block is the smallest configuration that lets UploadCN work:
PUTonly. The browser sends one kind of request to the bucket: aPUTto a URL your server signed, for a whole file or for one part of a multipart upload. Reads, deletes and multipart bookkeeping happen on your server.content-typeonly. It is the single header the browser sends, and the signature locks its value.ETagexposed. Multipart uploads read each part'sETagresponse header to finish the upload. Without it, files at or above the multipart threshold (64 MiB by default) fail on their first part with a “Missing ETag” error.- Object permissions only. The access key needs to put, read and delete objects, and to abort and list the parts of multipart uploads. It never needs to list or manage buckets, and the bucket can stay private.
Checked against the providers' docs
The formats come from each provider's official documentation, linked under every
result. They were last checked in November 2026. Two things could not be confirmed
in a provider's docs and are marked as notes in the output: ExposeHeaders on
Tigris (its CORS page does not show the field), and how Cloudinary treats browser
origins (its docs describe no CORS setting).
After you paste
Add the route and point your components at it
npx shadcn@latest add @uploadcn/upload-route @uploadcn/upload-configCloudinary needs no route for unsigned uploads: use the adapter block from the generator instead.
Restart the dev server
Next.js reads .env.local at startup.
Upload a small file, then a large one
A small file checks the signature and CORS. A file over 64 MiB also checks multipart
and the exposed ETag. If the browser console shows a CORS error, compare the origin
in the error with the ones in your rule: scheme, host and port must match exactly.