The fastest way to add file uploads to any web application. Enterprise security, edge-ready.
Upload files directly to S3-compatible storage with just 3 lines of code. No heavy AWS SDK dependencies - works with Next.js, React, Express, Fastify, and more. Built by Abhay Ramesh.
- 🚀 Lightning Fast - Optimized bundles with tree-shaking support
- 🪶 Ultra Lightweight - No heavy AWS SDK bloat, minimal dependencies
- 🎯 Type Safe - Full TypeScript support with intelligent inference
- ☁️ Multi-Provider - AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO
- 🎨 Framework Agnostic - Next.js, Express, Fastify, and more
- 📱 Modern React - Hooks and utilities for seamless integration
- 🔒 Enterprise Security - Presigned URLs, CORS handling, file validation
- 🌍 Edge Runtime - Runs on Vercel Edge, Cloudflare Workers, and more
- 📊 Progress Tracking - Real-time progress, upload speed, and ETA estimation
- 🔄 Lifecycle Callbacks - Complete upload control with
onStart,onProgress,onSuccess, andonError - 🗄️ Storage Operations - Complete file management API (list, delete, metadata)
- 🛠️ CLI Tools - Interactive setup and project scaffolding
- 🛡️ Production Ready - Used by thousands of applications
# Install the core package
npm install pushduck
# or
pnpm add pushduck
# or
yarn add pushduck
# Optional: Install CLI for easy setup
npm install -g @pushduck/cli
pnpm add -g @pushduck/cli# Interactive setup (recommended)
npx @pushduck/cli@latest init
# Add upload route to existing project
npx @pushduck/cli add-route
# Test your S3 connection
npx @pushduck/cli testStep 1: Create API Route (app/api/upload/route.ts)
import { createUploadConfig } from "pushduck/server";
const { s3 } = createUploadConfig()
.provider("aws", {
bucket: process.env.AWS_BUCKET_NAME!,
region: process.env.AWS_REGION!,
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
})
.build();
const router = s3.createRouter({
imageUpload: s3.image().maxFileSize('5MB'),
});
export const { GET, POST } = router.handlers;
export type AppRouter = typeof router;Step 2: Create Upload Client (lib/upload-client.ts)
import { createUploadClient } from "pushduck/client";
import type { AppRouter } from "@/app/api/upload/route";
export const upload = createUploadClient<AppRouter>({
endpoint: "/api/upload"
});Step 3: Use in Component (app/upload.tsx)
"use client";
import { upload } from "@/lib/upload-client";
export default function Upload() {
const { uploadFiles, files, isUploading } = upload.imageUpload();
return (
<div>
<input
type="file"
multiple
accept="image/*"
onChange={(e) => uploadFiles(Array.from(e.target.files || []))}
disabled={isUploading}
/>
{files.map((file) => (
<div key={file.id}>
{file.name} - {file.progress}%
{file.status === "success" && <img src={file.url} alt={file.name} />}
</div>
))}
</div>
);
}Done! 3 files, ~50 lines of code, production-ready uploads.
import { storage } from "pushduck/storage";
// List files with filtering
const files = await storage.list.files({
prefix: "uploads/",
maxResults: 50,
sortBy: "lastModified"
});
// Get file metadata
const fileInfo = await storage.metadata.getInfo("uploads/image.jpg");
console.log(fileInfo.size, fileInfo.lastModified, fileInfo.contentType);
// Delete operations
await storage.delete.file("uploads/old-file.jpg");
await storage.delete.byPrefix("temp/"); // Delete all files with prefix
await storage.delete.files(["file1.jpg", "file2.pdf"]); // Batch delete
// Generate download URLs
const downloadUrl = await storage.download.presignedUrl("uploads/document.pdf", 3600);
// Advanced listing with pagination
for await (const batch of storage.list.paginatedGenerator({ maxResults: 100 })) {
console.log(`Processing ${batch.files.length} files`);
// Process large datasets efficiently
}
// Filter by file properties
const images = await storage.list.byExtension("jpg", "photos/");
const largeFiles = await storage.list.bySize(1024 * 1024); // Files > 1MB
const recentFiles = await storage.list.byDate(new Date("2024-01-01"));- Getting Started - Complete setup guide
- Philosophy & Scope - What we do (and don't do)
- API Reference - Full API documentation
- Examples - Real-world examples
- Providers - S3, R2, Spaces, MinIO
- Security - Security best practices
- CLI Guide - CLI commands and usage
// 200+ lines of boilerplate code
// Heavy AWS SDK dependencies (2MB+ bundle size)
// Manual presigned URL generation
// CORS configuration headaches
// Security vulnerabilities
// Framework-specific implementations// 3 lines of code + ultra-lightweight (no heavy AWS SDK)
const { uploadFiles } = upload.imageUpload();
await uploadFiles(selectedFiles);Unlike other solutions that bundle the entire AWS SDK (2MB+), Pushduck uses aws4fetch - a tiny, zero-dependency AWS request signer that works everywhere:
- ✅ Tiny Bundle - Only 1 dependency, works on edge runtimes
- ✅ Zero Dependencies -
aws4fetchhas no dependencies itself - ✅ Edge Compatible - Runs on Vercel Edge, Cloudflare Workers, Deno Deploy
- ✅ Modern Fetch - Uses native
fetch()API, no legacy HTTP clients - ✅ Tree Shakeable - Only import what you need
// What you get with Pushduck
import { createUploadConfig } from "pushduck/server"; // ~5KB
// vs other solutions
import { S3Client } from "@aws-sdk/client-s3"; // ~500KB+Pushduck follows a secure-by-default architecture:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Your Client │ │ Your Server │ │ S3 Storage │
│ │ │ │ │ │
│ 1. Select File │───▶│ 2. Generate │───▶│ 3. Direct │
│ │ │ Presigned │ │ Upload │
│ 4. Upload to S3 │◀───│ URL │ │ │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
- Client never sees your AWS credentials
- Server generates secure, time-limited upload URLs
- Files upload directly to S3 (no server bandwidth used)
- Edge Compatible - runs anywhere modern JavaScript runs
import { createUploadConfig } from "pushduck/server";
const { s3 } = createUploadConfig()
.provider("aws", {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: process.env.AWS_REGION!,
bucket: process.env.AWS_S3_BUCKET_NAME!,
})
.defaults({
maxFileSize: "10MB",
acl: "public-read",
})
.paths({
prefix: "uploads",
generateKey: (file, metadata) => {
const userId = metadata.userId || "anonymous";
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 8);
return `${userId}/${timestamp}/${randomId}/${file.name}`;
},
})
.security({
allowedOrigins: ["https://yourdomain.com"],
rateLimiting: {
maxUploads: 10,
windowMs: 60000, // 1 minute
},
})
.hooks({
onUploadComplete: async ({ file, url, metadata }) => {
// Save to database, send notifications, etc.
console.log(`✅ Upload complete: ${file.name} -> ${url}`);
},
})
.build();
const router = s3.createRouter({
imageUpload: s3
.image()
.maxFileSize("5MB")
.formats(["jpeg", "jpg", "png", "webp"])
.middleware(async ({ file, metadata }) => {
// Add authentication and user context
const user = await authenticateUser(req);
return {
...metadata,
userId: user.id,
uploadedAt: new Date().toISOString(),
};
}),
});// Next.js App Router (default)
import { createUploadConfig } from "pushduck/server";// AWS S3
const { s3: awsS3 } = createUploadConfig()
.provider("aws", {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
region: process.env.AWS_REGION!,
bucket: process.env.AWS_S3_BUCKET_NAME!,
})
.build();
// Cloudflare R2 (S3-compatible)
const { s3: r2S3 } = createUploadConfig()
.provider("cloudflareR2", {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
accessKeyId: process.env.CLOUDFLARE_ACCESS_KEY_ID!,
secretAccessKey: process.env.CLOUDFLARE_SECRET_ACCESS_KEY!,
bucket: process.env.CLOUDFLARE_BUCKET_NAME!,
region: "auto",
})
.build();
// DigitalOcean Spaces (S3-compatible)
const { s3: spacesS3 } = createUploadConfig()
.provider("digitalOceanSpaces", {
accessKeyId: process.env.DO_SPACES_ACCESS_KEY_ID!,
secretAccessKey: process.env.DO_SPACES_SECRET_ACCESS_KEY!,
region: process.env.DO_SPACES_REGION!,
bucket: process.env.DO_SPACES_BUCKET_NAME!,
})
.build();
// MinIO (S3-compatible)
const { s3: minioS3 } = createUploadConfig()
.provider("minio", {
endpoint: process.env.MINIO_ENDPOINT!,
accessKeyId: process.env.MINIO_ACCESS_KEY_ID!,
secretAccessKey: process.env.MINIO_SECRET_ACCESS_KEY!,
bucket: process.env.MINIO_BUCKET_NAME!,
useSSL: false,
})
.build();Pushduck works with all major frameworks:
- Next.js - App Router, Pages Router
- Express - RESTful APIs
- Fastify - High-performance APIs
- Remix - Full-stack React
- SvelteKit - Svelte applications
- Nuxt - Vue applications
- Astro - Static site generation
- Hono - Edge runtime APIs
| Package | Description | Version |
|---|---|---|
pushduck |
Core library | |
@pushduck/cli |
CLI tools | |
@pushduck/ui |
React components |
We love contributions! Please read our Contributing Guide to get started.
git clone https://github.com/abhay-ramesh/pushduck.git
cd pushduck
pnpm install
pnpm devpnpm dev # Start development servers
pnpm build # Build all packages
pnpm test # Run test suite
pnpm lint # Lint code
pnpm type-check # TypeScript type checking
pnpm format # Format code with PrettierMIT © Abhay Ramesh
Built with ❤️ using:
- TypeScript
- aws4fetch - Lightweight AWS signing (the secret sauce!)
- ⭐ Star us on GitHub — it helps!
- 🐛 Report bugs — Create an issue
- 💡 Request features — Start a discussion
- 📧 Contact — [email protected]
