Direct-to-S3 multipart upload
Your server signs a handful of short-lived URLs; the browser uploads each part directly to S3. The bytes never transit your app server. Works against AWS S3, MinIO, Backblaze B2, Cloudflare R2, Wasabi - any S3-API-compatible backend. Survives reloads via IndexedDB: the resumed task reuses the existing uploadId and already-collected part ETags, so only the missing parts are re-uploaded.
Backend required. This demo needs 4 signing endpoints (
/s3/create, /s3/sign, /s3/complete, /s3/abort) that live on your server. MultipleUpload is the client; pair it with whichever server you use.
- Using ASP.NET Core? CoreUpload ships the four endpoints + a reference
IS3Signer. - Using Node.js / Express? See the Node backend sample - add the 4 routes using
@aws-sdk/client-s3+@aws-sdk/s3-request-presigner. - Using PHP / Laravel / Django / Rails? Same pattern - sign with the official AWS SDK for your language.
Client configuration
new MultipleUpload('#uploader', {
uploadUrl: '/api/upload', // only used for the base-URL derivation
strategy: 's3',
chunkSize: 5 * 1024 * 1024, // S3 minimum 5 MB part
chunkConcurrency: 4, // parallel PUTs
persistState: true,
persistAdapter: 'indexeddb' // resume survives reload
// Endpoint URLs derive from uploadUrl by default:
// /api/s3/create, /api/s3/sign, /api/s3/complete, /api/s3/abort
// Override any of them with s3CreateUrl, s3SignUrl, s3CompleteUrl, s3AbortUrl.
});Required backend endpoints
POST /s3/create<-{ fileName, fileSize, contentType }->{ uploadId, key }POST /s3/sign<-{ uploadId, key, partNumbers:[1..N] }->{ urls: { "1": "https://...", ... } }PUT <signedUrl>(browser-direct to S3) -> S3 returnsETagper partPOST /s3/complete<-{ uploadId, key, parts:[{PartNumber, ETag}] }-> final UploadResultPOST /s3/aborton cancel / fatal error
Node/Express reference (sketch)
import { S3Client, CreateMultipartUploadCommand, CompleteMultipartUploadCommand,
AbortMultipartUploadCommand, UploadPartCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const s3 = new S3Client({ region: 'us-east-1' });
const BUCKET = 'your-bucket';
app.post('/s3/create', async (req, res) => {
const key = `uploads/${crypto.randomUUID()}/${req.body.fileName}`;
const out = await s3.send(new CreateMultipartUploadCommand({ Bucket: BUCKET, Key: key }));
res.json({ uploadId: out.UploadId, key });
});
app.post('/s3/sign', async (req, res) => {
const urls = {};
for (const n of req.body.partNumbers) {
urls[n] = await getSignedUrl(s3, new UploadPartCommand({
Bucket: BUCKET, Key: req.body.key, UploadId: req.body.uploadId, PartNumber: n
}), { expiresIn: 15 * 60 });
}
res.json({ urls });
});
// /s3/complete and /s3/abort follow the same pattern.Bucket CORS
The browser PUTs directly to S3, so the bucket must allow cross-origin PUT and expose ETag:
[{
"AllowedOrigins": ["https://your-site.example"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}]