Works with any backend npm install Zero dependencies

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 returns ETag per part
  • POST /s3/complete <- { uploadId, key, parts:[{PartNumber, ETag}] } -> final UploadResult
  • POST /s3/abort on 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
}]