# AI & LLM Integration (/docs/ai-integration) ## AI & LLM Features Pushduck documentation provides AI-friendly endpoints that make it easy for large language models (LLMs) and automated tools to access and process our documentation content. ## Available Endpoints ### Complete Documentation Export Access all documentation content in a single, structured format: ``` GET /llms.txt ``` This endpoint returns all documentation pages in a clean, AI-readable format with: * Page titles and URLs * Descriptions and metadata * Full content with proper formatting * Structured sections and hierarchies **Example Usage:** ```bash curl https://your-domain.com/llms.txt ``` ### Individual Page Access Access any documentation page's raw content by appending `.mdx` to its URL: ``` GET /docs/{page-path}.mdx ``` **Examples:** * `/docs/quick-start.mdx` - Quick start guide content * `/docs/api/client/use-upload-route.mdx` - Hook documentation * `/docs/providers/aws-s3.mdx` - AWS S3 setup guide ## Use Cases ### **AI Assistant Integration** * Train custom AI models on our documentation * Create chatbots that can answer questions about Pushduck * Build intelligent documentation search systems ### **Development Tools** * Generate code examples and snippets * Create automated documentation tests * Build CLI tools that reference our docs ### **Content Analysis** * Analyze documentation completeness * Track content changes over time * Generate documentation metrics ## Content Format The LLM endpoints return content in a structured format: ``` # Page Title URL: /docs/page-path Page description here # Section Headers Content with proper markdown formatting... ## Subsections - Lists and bullet points - Code blocks with syntax highlighting - Tables and structured data ``` ## Technical Details * **Caching**: Content is cached for optimal performance * **Processing**: Uses Remark pipeline with MDX and GFM support * **Format**: Clean markdown with frontmatter removed * **Encoding**: UTF-8 text format * **CORS**: Enabled for cross-origin requests ## Rate Limiting These endpoints are designed for programmatic access and don't have aggressive rate limiting. However, please be respectful: * Cache responses when possible * Avoid excessive automated requests * Use appropriate user agents for your tools ## Examples ### Python Script ```python import requests # Get all documentation response = requests.get('https://your-domain.com/llms.txt') docs_content = response.text # Get specific page page_response = requests.get('https://your-domain.com/docs/quick-start.mdx') page_content = page_response.text ``` ### Node.js/JavaScript ```javascript // Fetch all documentation const allDocs = await fetch("/llms.txt").then((r) => r.text()); // Fetch specific page const quickStart = await fetch("/docs/quick-start.mdx").then((r) => r.text()); ``` ### cURL ```bash # Download all docs to file curl -o pushduck-docs.txt https://your-domain.com/llms.txt # Get specific page content curl https://your-domain.com/docs/api/client/use-upload-route.mdx ``` ## Integration with Popular AI Tools ### OpenAI GPT Use the `/llms.txt` endpoint to provide context about Pushduck in your GPT conversations. ### Claude/Anthropic Feed documentation content to Claude for detailed analysis and code generation. ### Local LLMs Download content for training or fine-tuning local language models. *** These AI-friendly endpoints make it easy to integrate Pushduck documentation into your development workflow and AI-powered tools! # Comparisons (/docs/comparisons) import { Callout } from "fumadocs-ui/components/callout"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; ## Overview Choosing the right file upload solution depends on your project's requirements. This page compares Pushduck with popular alternatives to help you make an informed decision. **TL;DR:** Pushduck is ideal if you want a **lightweight, self-hosted** solution with **full control** over your S3 storage, without vendor lock-in or ongoing upload fees. **Note:** Pricing, features, and bundle sizes are approximate and current as of October 2025. Always verify current details from official sources before making decisions. *** ## Quick Comparison | Feature | Pushduck | UploadThing | Uploadcare | AWS SDK | Uppy | | --------------------- | ---------------------------------------- | -------------------- | -------------------- | -------------------- | -------------------- | | **Bundle Size** | \~7KB | \~200KB+ | \~150KB+ | \~500KB+ | \~50KB+ (core) | | **Setup Time** | 5 minutes | 10 minutes | 15 minutes | 15-20 hours | 30-60 minutes | | **Edge Runtime** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No | ✅ Partial | | **Self-Hosted** | ✅ Yes | ❌ No | ❌ No | ✅ Yes | ✅ Yes | | **Pricing Model** | Free (S3 costs only) | Per upload | Per upload + storage | Free (S3 costs only) | Free (S3 costs only) | | **Type Safety** | ✅ Full | ✅ Full | ⚠️ Partial | ⚠️ Manual | ❌ None | | **Multi-Provider** | ✅ 6 (AWS, R2, DO, MinIO, GCS, S3-compat) | ❌ Own infrastructure | ❌ Own infrastructure | ❌ AWS only | ✅ Yes | | **React Hooks** | ✅ Built-in | ✅ Built-in | ✅ Built-in | ❌ Build yourself | ✅ Available | | **Progress Tracking** | ✅ Automatic | ✅ Automatic | ✅ Automatic | ❌ Build yourself | ✅ Automatic | | **Presigned URLs** | ✅ Automatic | ✅ Automatic | N/A (managed) | ❌ Build yourself | ⚠️ Manual | | **Best For** | Developers | Rapid prototyping | Enterprises | Full AWS control | UI flexibility | *** ## Detailed Comparisons ### vs UploadThing **UploadThing** is a managed file upload service with tight Next.js integration and developer-friendly DX. **When to choose Pushduck:** * ✅ You want to avoid per-upload fees * ✅ You need edge runtime support (UploadThing requires Node.js runtime) * ✅ You want full control over storage and file URLs * ✅ You're using multiple S3-compatible providers (R2, DigitalOcean, etc.) **When to choose UploadThing:** * ✅ You want zero infrastructure setup * ✅ You prefer managed service over self-hosted * ✅ You're building a rapid prototype or MVP ``` Pushduck: ~7KB (minified + gzipped) UploadThing: ~200KB+ (includes server runtime) Difference: 28x smaller ``` **Why Pushduck is smaller:** * Uses `aws4fetch` (lightweight AWS signer) instead of heavy dependencies * No built-in UI components (bring your own) * Focused on upload logic only * Optimized for tree-shaking **Pushduck:** * Library: **Free (MIT license)** * Costs: **S3 storage only** (\~$0.023/GB on AWS, free tier: 5GB + 20k requests/month) * Example: 10k uploads/month @ 2MB each = \~$0.50/month **UploadThing:** * Free tier: 2GB storage + 100 uploads/month * Pro: $20/month (50GB storage + 10k uploads) * Enterprise: Custom pricing **Cost Comparison (10k monthly uploads):** * Pushduck + AWS S3: **\~$0.50/month** * UploadThing Pro: **$20/month** (if within limits) | Aspect | Pushduck | UploadThing | | -------------------- | ------------------------- | ------------------------------ | | **Storage Provider** | Your choice (6 providers) | UploadThing's infrastructure | | **File URLs** | Your domain/CDN | UploadThing's CDN | | **Data Ownership** | 100% yours | Stored on their infrastructure | | **Migration** | Easy (standard S3) | Requires re-uploading files | | **Vendor Lock-in** | None | Medium | *** ### vs AWS SDK **AWS SDK (`@aws-sdk/client-s3`)** is the official AWS library for S3 operations. **When to choose Pushduck:** * ✅ You need edge runtime support (AWS SDK requires Node.js) * ✅ You want a smaller bundle (\~16KB vs \~500KB) * ✅ You need React hooks and type-safe APIs * ✅ You want multi-provider support (R2, DigitalOcean, MinIO) * ✅ You prefer declarative schemas over imperative code * ✅ You want presigned URLs handled automatically * ✅ You want to avoid implementing upload infrastructure from scratch **When to choose AWS SDK:** * ✅ You need advanced S3 features (lifecycle policies, bucket management) * ✅ You're already heavily invested in AWS ecosystem * ✅ You need multipart uploads for very large files (100GB+) * ✅ You need direct, low-level control over every S3 operation ``` Pushduck: ~7KB (core client, minified + gzipped) AWS SDK: ~500KB (@aws-sdk/client-s3) Difference: 71x smaller ⚡️ ``` **Why it matters:** * Faster page loads * Lower bandwidth costs * Better mobile experience * Improved Core Web Vitals **Pushduck:** ```typescript // Declarative schema const router = s3.createRouter({ imageUpload: s3.image() .maxFileSize('5MB') .middleware(async ({ req }) => { const user = await auth(req); return { userId: user.id }; }), }); // Client (React) const { uploadFiles } = useUploadRoute('imageUpload'); ``` **AWS SDK:** ```typescript // Imperative code import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'us-east-1' }); const uploadFile = async (file: File) => { const command = new PutObjectCommand({ Bucket: 'my-bucket', Key: `uploads/${file.name}`, Body: await file.arrayBuffer(), ContentType: file.type, }); await s3.send(command); }; // + Manual progress tracking // + Manual validation // + Manual React state management ``` **Pushduck provides:** * Type-safe schemas * Built-in React hooks * Automatic progress tracking * Middleware system * Multi-provider support **AWS SDK provides:** * Direct S3 control * Advanced features * Official AWS support ### What AWS SDK Requires You to Build With AWS SDK, you need to **manually implement everything**: #### 1. Presigned URL Generation ```typescript import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; // ❌ You must handle: // - Creating S3 client with credentials // - Generating unique file keys // - Setting correct content types // - Configuring expiration times // - Handling CORS headers // - Managing bucket permissions const s3Client = new S3Client({ region: 'us-east-1' }); const generatePresignedUrl = async (fileName: string) => { const key = `uploads/${Date.now()}-${fileName}`; const command = new PutObjectCommand({ Bucket: 'my-bucket', Key: key, ContentType: 'application/octet-stream', // Must set manually }); const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); return { url, key }; }; ``` #### 2. Client-Side Upload Logic ```typescript // ❌ You must build: // - XMLHttpRequest wrapper for progress tracking // - Error handling and retry logic // - AbortController for cancellation // - State management for multiple files // - Progress aggregation // - File validation (size, type) const uploadFile = (file: File, presignedUrl: string) => { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => { const progress = (e.loaded / e.total) * 100; // Update UI manually }; xhr.onload = () => { if (xhr.status === 200) { resolve(xhr.response); } else { reject(new Error('Upload failed')); } }; xhr.onerror = () => reject(new Error('Network error')); xhr.open('PUT', presignedUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.send(file); }); }; ``` #### 3. API Route Handler ```typescript // ❌ You must implement: // - Request parsing and validation // - Authentication/authorization // - File metadata validation // - Error responses // - Type safety export async function POST(request: Request) { const { fileName, fileSize, fileType } = await request.json(); // Validate manually if (fileSize > 10 * 1024 * 1024) { return Response.json({ error: 'File too large' }, { status: 400 }); } // Auth manually const user = await authenticateUser(request); if (!user) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); } // Generate presigned URL const { url, key } = await generatePresignedUrl(fileName); return Response.json({ url, key }); } ``` #### 4. React Component State Management ```typescript // ❌ You must manage: // - File state (idle, uploading, success, error) // - Progress for each file // - Overall progress // - Error messages // - Upload speed and ETA calculations // - Cleanup on unmount const [files, setFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [progress, setProgress] = useState(0); const [errors, setErrors] = useState([]); // Implement all the upload logic... ``` #### 5. CORS Configuration ```json // ❌ You must configure S3 bucket CORS manually: { "CORSRules": [ { "AllowedOrigins": ["https://your-domain.com"], "AllowedMethods": ["PUT", "POST", "GET"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"] } ] } ``` *** ### What Pushduck Handles For You ```typescript // ✅ Pushduck handles ALL of the above: // Server (3 lines) const router = s3.createRouter({ imageUpload: s3.image().maxFileSize('5MB'), }); export const { GET, POST } = router.handlers; // Client (1 line) const { uploadFiles, progress, isUploading } = useUploadRoute('imageUpload'); ``` **Everything included:** * ✅ Presigned URL generation (automatic) * ✅ File validation (declarative) * ✅ Progress tracking (real-time) * ✅ Error handling (built-in) * ✅ Multi-file support (automatic) * ✅ React state management (handled) * ✅ Type safety (end-to-end) * ✅ Authentication hooks (middleware) * ✅ CORS headers (automatic) * ✅ Cancellation (AbortController) *** ### Time to Production | Task | AWS SDK | Pushduck | | ---------------------- | ----------------- | ---------------- | | **Initial setup** | 2-4 hours | 5 minutes | | **Progress tracking** | 1-2 hours | Included | | **Error handling** | 1-2 hours | Included | | **Multi-file uploads** | 2-3 hours | Included | | **Type safety** | 2-4 hours | Included | | **Testing** | 4-6 hours | Minimal | | **Total** | **\~15-20 hours** | **\~30 minutes** | **AWS SDK is a low-level tool.** You're responsible for building the entire upload infrastructure, handling edge cases, security, validation, progress tracking, and state management. **Pushduck is a high-level framework.** All the infrastructure is built-in, tested, and production-ready out of the box. *** ### vs Uploadcare / Filestack **Uploadcare** and **Filestack** are managed file upload platforms with built-in CDN, transformations, and processing. **When to choose Pushduck:** * ✅ You want to avoid per-upload and storage fees * ✅ You need full control over file storage and URLs * ✅ You prefer self-hosted over managed services * ✅ You don't need built-in image processing (can integrate Sharp, Cloudinary, etc.) * ✅ You want to avoid vendor lock-in **When to choose Uploadcare/Filestack:** * ✅ You need built-in image/video processing * ✅ You want zero infrastructure management * ✅ You need global CDN with automatic optimization * ✅ You have budget for managed services **Pushduck:** * Library: **Free** * Storage: **S3 costs** (\~$0.023/GB on AWS) * CDN: **Optional** (CloudFront, Cloudflare, BunnyCDN) * Processing: **Integrate your choice** (Sharp, Cloudinary, Imgix) **Uploadcare:** * Free: 3k uploads + 3GB storage/month * Start: $25/month (10k uploads + 10GB) * Pro: $99/month (50k uploads + 100GB) * Enterprise: Custom **Filestack:** * Free: 100 uploads + 100 transformations/month * Starter: $49/month (1k uploads) * Professional: $249/month (10k uploads) **Cost Example (10k monthly uploads, 20GB storage):** * Pushduck + S3: **\~$0.50/month** (+ optional CDN \~$1-5) * Uploadcare Start: **$25/month** * Filestack Professional: **$249/month** | Aspect | Pushduck | Uploadcare/Filestack | | ------------------ | ------------------- | -------------------- | | **Storage** | Your S3 bucket | Their infrastructure | | **File URLs** | Your domain | Their CDN | | **Processing** | Integrate as needed | Built-in | | **Data Migration** | Standard S3 API | Proprietary API | | **Privacy** | Full control | Trust third party | | **Vendor Lock-in** | None | High | *** ### vs Uppy **Uppy** is a modular JavaScript file uploader with a focus on UI components and extensibility. **When to choose Pushduck:** * ✅ You're using React (Pushduck has first-class React support) * ✅ You need type-safe APIs with TypeScript inference * ✅ You want tighter Next.js integration * ✅ You prefer schema-based validation over manual configuration **When to choose Uppy:** * ✅ You need a rich, pre-built UI (Dashboard, Drag & Drop, Webcam, Screen Capture) * ✅ You're using vanilla JS or other frameworks (Vue, Svelte) * ✅ You need resumable uploads (tus protocol) * ✅ You want highly customizable UI components **Pushduck:** * **Focus:** Direct-to-S3 uploads with presigned URLs * **UI:** Bring your own (minimal bundle size) * **Type Safety:** First-class TypeScript, runtime validation * **Backend:** Server-first (schema definitions, middleware, hooks) **Uppy:** * **Focus:** Modular file uploader with rich UI * **UI:** Built-in components (Dashboard, Drag & Drop, etc.) * **Type Safety:** TypeScript definitions available * **Backend:** Agnostic (works with any backend) **Choose Pushduck if:** ```typescript // You want schema-based validation const router = s3.createRouter({ profilePic: s3.image() .maxFileSize('2MB') .accept(['image/jpeg', 'image/png']) .middleware(auth) .onUploadComplete(updateDatabase), }); ``` **Choose Uppy if:** ```typescript // You want rich UI out of the box import Uppy from '@uppy/core'; import Dashboard from '@uppy/dashboard'; import Webcam from '@uppy/webcam'; import ScreenCapture from '@uppy/screen-capture'; const uppy = new Uppy() .use(Dashboard, { inline: true }) .use(Webcam, { target: Dashboard }) .use(ScreenCapture, { target: Dashboard }); ``` *** ## Decision Matrix ### Choose Pushduck if: ✅ You want **full control** over storage and infrastructure\ ✅ You need **edge runtime** compatibility (Vercel, Cloudflare Workers)\ ✅ You prefer **self-hosted** over managed services\ ✅ You want to **minimize costs** (pay only S3 storage fees)\ ✅ You need **type-safe APIs** with TypeScript inference\ ✅ You're building with **React** and **Next.js**\ ✅ You want to avoid **vendor lock-in**\ ✅ You need support for **multiple S3-compatible providers** *** ### Choose UploadThing if: ✅ You want **zero infrastructure setup**\ ✅ You prefer **managed service** over self-hosting\ ✅ You're building a **rapid prototype** or **MVP**\ ✅ You want to **avoid S3 configuration**\ ✅ You're **okay with per-upload pricing** *** ### Choose AWS SDK if: ✅ You need **advanced S3 features** (lifecycle, versioning, bucket management)\ ✅ You're **AWS-only** (not using other providers)\ ✅ You need **multipart uploads** for very large files (100GB+)\ ✅ You don't need **edge runtime** compatibility\ ✅ Bundle size is **not a concern**\ ✅ You have **15-20 hours** to build upload infrastructure from scratch\ ✅ You want **full low-level control** over every S3 operation\ ⚠️ You're comfortable **manually implementing** presigned URLs, progress tracking, validation, error handling, and React state management *** ### Choose Uploadcare/Filestack if: ✅ You need **built-in image/video processing**\ ✅ You want **zero infrastructure** management\ ✅ You need a **global CDN** with automatic optimization\ ✅ You have **budget for managed services** ($25-250/month)\ ✅ You want **all-in-one** (upload + storage + processing + CDN) *** ### Choose Uppy if: ✅ You need **rich, pre-built UI** components\ ✅ You want **highly customizable** upload widgets\ ✅ You need **resumable uploads** (tus protocol)\ ✅ You're **not using React** (vanilla JS, Vue, Svelte)\ ✅ You want **modular architecture** (pick and choose plugins) *** ## Feature Comparison ### Core Features | Feature | Pushduck | UploadThing | AWS SDK | Uploadcare | Uppy | | --------------------- | -------- | ----------- | ---------- | ---------- | ---------- | | **Direct-to-S3** | ✅ | ✅ | ✅ | ❌ | ✅ | | **Presigned URLs** | ✅ | ✅ | ✅ | ❌ | ⚠️ | | **Progress Tracking** | ✅ | ✅ | ⚠️ Manual | ✅ | ✅ | | **Multi-file Upload** | ✅ | ✅ | ✅ | ✅ | ✅ | | **Type Safety** | ✅ Full | ✅ Full | ⚠️ Partial | ⚠️ Partial | ⚠️ Partial | | **Schema Validation** | ✅ | ✅ | ❌ | ⚠️ | ⚠️ | | **Middleware System** | ✅ | ✅ | ❌ | ❌ | ⚠️ | | **Lifecycle Hooks** | ✅ | ✅ | ❌ | ⚠️ | ✅ | | **React Hooks** | ✅ | ✅ | ❌ | ✅ | ✅ | ### Storage & Providers | Feature | Pushduck | UploadThing | AWS SDK | Uploadcare | Uppy | | ------------------------ | -------- | ----------- | ------- | ---------- | ---- | | **AWS S3** | ✅ | ❌ | ✅ | ❌ | ✅ | | **Cloudflare R2** | ✅ | ❌ | ❌ | ❌ | ✅ | | **DigitalOcean Spaces** | ✅ | ❌ | ❌ | ❌ | ✅ | | **Google Cloud Storage** | ✅ | ❌ | ❌ | ❌ | ✅ | | **MinIO** | ✅ | ❌ | ❌ | ❌ | ✅ | | **Backblaze B2** | ✅ | ❌ | ❌ | ❌ | ✅ | | **Custom Domain** | ✅ | ⚠️ Limited | ✅ | ✅ | ✅ | ### Runtime & Compatibility | Feature | Pushduck | UploadThing | AWS SDK | Uploadcare | Uppy | | ------------------------ | -------- | ----------- | ------- | ---------- | ---- | | **Edge Runtime** | ✅ | ✅ | ❌ | ✅ | ✅ | | **Node.js** | ✅ | ✅ | ✅ | ✅ | ✅ | | **Cloudflare Workers** | ✅ | ✅ | ❌ | ✅ | ✅ | | **Vercel Edge** | ✅ | ✅ | ❌ | ✅ | ✅ | | **Next.js App Router** | ✅ | ✅ | ✅ | ✅ | ✅ | | **Next.js Pages Router** | ✅ | ✅ | ✅ | ✅ | ✅ | | **Remix** | ✅ | ⚠️ | ✅ | ✅ | ✅ | | **SvelteKit** | ✅ | ❌ | ✅ | ✅ | ✅ | *** ## Bundle Size Breakdown ``` ┌────────────────────────────────────────────────┐ │ Bundle Size Comparison (minified + gzipped) │ ├────────────────────────────────────────────────┤ │ Pushduck: ██ 7KB │ │ Uppy (core): ████████████ 50KB │ │ Uploadcare: ██████████████████ 150KB │ │ UploadThing: ████████████████████████ 200KB │ │ AWS SDK: ████████████████████████ 500KB │ └────────────────────────────────────────────────┘ ``` **Why bundle size matters:** * **Faster initial page load** (especially on mobile) * **Lower bandwidth costs** * **Better Core Web Vitals** (LCP, FCP) * **Improved SEO** (Google considers page speed) *** ## Pricing Breakdown ### Monthly Cost Example **Scenario:** 10,000 uploads/month, 2MB average file size, 20GB total storage | Solution | Monthly Cost | Breakdown | | ---------------------------- | ------------ | ---------------------------------- | | **Pushduck + AWS S3** | **$0.50** | Storage: $0.46, Requests: $0.04 | | **Pushduck + Cloudflare R2** | **$0.30** | Storage: $0.30, Egress: $0 | | **AWS SDK + S3** | **$0.50** | Same as Pushduck (library is free) | | **UploadThing Pro** | **$20** | (if within 10k upload limit) | | **Uploadcare Start** | **$25** | (if within 10k upload limit) | | **Filestack Professional** | **$249** | (10k uploads tier) | **Note:** Pushduck and AWS SDK are libraries, not services. You pay only for S3 storage. Managed services (UploadThing, Uploadcare, Filestack) handle infrastructure but charge per-upload fees. *** ## Migration Guide ### From AWS SDK to Pushduck ```typescript // Before (AWS SDK) import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'us-east-1' }); const uploadFile = async (file: File) => { const command = new PutObjectCommand({ Bucket: 'my-bucket', Key: `uploads/${file.name}`, Body: await file.arrayBuffer(), }); await s3.send(command); }; // After (Pushduck) // Server const { s3 } = createUploadConfig() .provider('aws', { bucket: 'my-bucket', region: 'us-east-1' }) .build(); const router = s3.createRouter({ fileUpload: s3.file().maxFileSize('10MB'), }); export const { GET, POST } = router.handlers; // Client const { uploadFiles } = useUploadRoute('fileUpload'); ``` **Benefits:** * ✅ 31x smaller bundle (\~16KB vs \~500KB) * ✅ Edge runtime compatible * ✅ Built-in React hooks * ✅ Type-safe APIs * ✅ Automatic progress tracking *** ### From UploadThing to Pushduck **Migration Steps:** 1. **Set up S3 bucket** (one-time setup) 2. **Replace UploadThing config with Pushduck config** 3. **Update client imports** 4. **Migrate files** (optional, can use UploadThing's S3 export if available) **Data Ownership:** * UploadThing: Files on their infrastructure (requires export) * Pushduck: Files in your S3 bucket (you own them) *** ## Frequently Asked Questions ### "Should I use Pushduck or a managed service?" **Use Pushduck if:** * You want full control and ownership * You want to minimize costs * You're comfortable with basic S3 setup **Use managed service if:** * You want zero infrastructure setup * You need built-in processing (images, videos) * You have budget for convenience *** ### "Is Pushduck production-ready?" ✅ Yes! Pushduck is used in production by multiple projects. **Production features:** * Comprehensive error handling * Health checks and metrics * Battle-tested S3 upload flow * Type-safe APIs * Extensive test coverage See: [Production Checklist](/docs/guides/production-checklist) *** ### "Can I migrate from Pushduck later?" ✅ Yes, easily! Your files are in standard S3 buckets. **Migration path:** 1. Your files are already in S3 (standard format) 2. Switch to any S3-compatible solution 3. No data migration needed (files stay in your bucket) 4. No vendor lock-in *** ### "Does Pushduck support image processing?" ⚠️ **Not built-in** (by design - keeps bundle small). **Integration options:** * [Sharp](/docs/guides/image-uploads) - Server-side processing * [Cloudinary](/docs/guides/image-uploads) - API-based * [Imgix](/docs/guides/image-uploads) - URL-based * Any image processing tool See: [Image Uploads Guide](/docs/guides/image-uploads) *** ## Conclusion **Pushduck** is ideal for developers who want: * 🪶 Lightweight library (\~16KB) * 🔒 Full control over storage * 💰 Minimal costs (S3 only) * 🚀 Edge runtime support * 🔌 No vendor lock-in If you need a managed service with built-in processing and global CDN, consider **Uploadcare** or **Filestack**. If you want rapid prototyping with zero S3 setup, consider **UploadThing**. For advanced S3 features and AWS-only projects, **AWS SDK** is the right choice. For rich UI components and resumable uploads, **Uppy** is a great option. *** **Ready to get started?** Head to the [Quick Start](/docs/quick-start) guide to set up Pushduck in 5 minutes. # Examples & Demos (/docs/examples) import { Callout } from "fumadocs-ui/components/callout"; import { Tabs, Tab } from "fumadocs-ui/components/tabs"; **Live Demos:** These are fully functional demos using real Cloudflare R2 storage. Files are uploaded to a demo bucket and may be automatically cleaned up. Don't upload sensitive information. **Having Issues?** If uploads aren't working (especially with `next dev --turbo`), check our [Troubleshooting Guide](/docs/api/troubleshooting) for common solutions including the known Turbo mode compatibility issue. ## Interactive Upload Demo The full-featured demo showcasing all capabilities: **ETA & Speed Tracking:** Upload speed (MB/s) and estimated time remaining (ETA) appear below the progress bar during active uploads. Try uploading larger files (1MB+) to see these metrics in action! ETA becomes more accurate after the first few seconds of upload. ## Image-Only Upload Focused demo for image uploads with preview capabilities: ## Document Upload Streamlined demo for document uploads: ## Key Features Demonstrated ### **Type-Safe Client** ```typescript // Property-based access with full TypeScript inference const imageUpload = upload.imageUpload(); const fileUpload = upload.fileUpload(); // No string literals, no typos, full autocomplete await imageUpload.uploadFiles(selectedFiles); ``` ### **Real-Time Progress** * Individual file progress tracking with percentage completion * Upload speed monitoring (MB/s) with live updates * ETA calculations showing estimated time remaining * Pause/resume functionality (coming soon) * Comprehensive error handling with retry mechanisms ### **Built-in Validation** * File type validation (MIME types) * File size limits with user-friendly errors * Custom validation middleware * Malicious file detection ### 🌐 **Provider Agnostic** * Same code works with any S3-compatible provider * Switch between Cloudflare R2, AWS S3, DigitalOcean Spaces * Zero vendor lock-in ## Code Examples ```typescript "use client"; import { upload } from "@/lib/upload-client"; export function SimpleUpload() { const { uploadFiles, files, isUploading } = upload.imageUpload(); return (
uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> {files.map(file => (
{file.name} {file.status} {file.url && View}
))}
); } ```
```typescript "use client"; import { upload } from "@/lib/upload-client"; import { useState } from "react"; export function MetadataUpload() { const [albumId, setAlbumId] = useState('vacation-2025'); const [tags, setTags] = useState(['summer']); const { uploadFiles, files, isUploading } = upload.imageUpload({ onSuccess: (results) => { console.log(`Uploaded ${results.length} images to album: ${albumId}`); } }); const handleUpload = (e: React.ChangeEvent) => { const selectedFiles = Array.from(e.target.files || []); // Pass client-side context as metadata uploadFiles(selectedFiles, { albumId: albumId, tags: tags, visibility: 'private', uploadSource: 'web-app' }); }; return (
{files.map(file => (
{file.name} {file.status} {file.url && View}
))}
); } ```
```typescript // app/api/upload/route.ts import { createUploadConfig } from "pushduck/server"; const { s3, } = createUploadConfig() .provider("cloudflareR2",{ accountId: process.env.CLOUDFLARE_ACCOUNT_ID!, bucket: process.env.R2_BUCKET!, }) .defaults({ maxFileSize: "10MB", acl: "public-read", }) .build(); const uploadRouter = s3.createRouter({ imageUpload: s3 .image() .maxFileSize("5MB") .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ file, metadata }) => { // Custom authentication and metadata const session = await getServerSession(); if (!session) throw new Error("Unauthorized"); return { ...metadata, userId: session.user.id, uploadedAt: new Date().toISOString(), }; }) .onUploadComplete(async ({ file, url, metadata }) => { // Post-upload processing console.log(`Upload complete: ${url}`); await saveToDatabase({ url, metadata }); }), }); export const { GET, POST } = uploadRouter.handlers; export type AppRouter = typeof uploadRouter; ``` ```typescript "use client"; import { upload } from "@/lib/upload-client"; export function RobustUpload() { const { uploadFiles, files, errors, reset } = upload.imageUpload(); const handleUpload = async (fileList: FileList) => { try { await uploadFiles(Array.from(fileList)); } catch (error) { console.error("Upload failed:", error); // Error is automatically added to the errors array } }; return (
e.target.files && handleUpload(e.target.files)} /> {/* Display errors */} {errors.length > 0 && (

Upload Errors:

{errors.map((error, index) => (

{error}

))}
)} {/* Display files with status */} {files.map(file => (
{file.name} {file.status} {file.status === "uploading" && ( )} {file.status === "error" && ( {file.error} )} {file.status === "success" && file.url && ( View File )}
))}
); } ```
## Real-World Use Cases ### **Profile Picture Upload** Single image upload with instant preview and crop functionality. ### **Document Management** Multi-file document upload with categorization and metadata. ### **Media Gallery** Batch image upload with automatic optimization and thumbnail generation. ### **File Sharing** Secure file upload with expiration dates and access controls. ## Next Steps
Quick Start
Get set up in 2 minutes with our CLI
API Reference
Complete API documentation
Providers
Configure your storage provider
Full Demo
Complete upload experience
# How It Works (/docs/how-it-works) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Mermaid } from "@/components/mdx/mermaid"; ## Architecture Overview Pushduck is built on a **direct-to-S3 upload pattern** using presigned URLs, eliminating the need for your server to handle file data. **Core Principle:** Files go directly from the client to S3 storage, bypassing your server entirely. This enables infinite scalability and edge-compatible deployments. *** ## Upload Flow ### Complete Upload Process **Key Benefits:** * ✅ Server never touches file data (saves bandwidth) * ✅ Scales infinitely (S3 handles the load) * ✅ Edge-compatible (no file streaming needed) * ✅ Real-time progress tracking on client *** ## Component Architecture *** ## Configuration Flow *** ## Type Safety System *** ## Middleware Chain *** ## Storage Provider System **Key Insight:** All providers use the same S3-compatible API, so switching is just a configuration change. *** ## Client State Management *** ## Integration Points *** ## Comparison: Pushduck vs AWS SDK ### What You Need to Build with AWS SDK *** ## Key Takeaways Files upload directly to S3 storage, bypassing your server. This enables infinite scalability and edge deployment. \~7KB total bundle using `aws4fetch` instead of AWS SDK (\~500KB). 71x smaller, edge-compatible. End-to-end TypeScript inference from server schema to client hook. Catch errors at compile-time. Middleware and lifecycle hooks provide integration points without bloating the library with built-in features. Universal Web Standard handlers work with 16+ frameworks via thin adapters. Unified API works with 6 S3-compatible providers. Switch providers with just a config change. *** ## Next Steps **Ready to build?** Check out the [Quick Start](/docs/quick-start) guide to get Pushduck running in 5 minutes. **Learn More:** * [Philosophy & Scope](/docs/philosophy) - What Pushduck does (and doesn't do) * [API Reference](/docs/api) - Complete API documentation * [Examples](/docs/examples) - Live demos and code samples # Pushduck (/docs) import { Card, Cards } from "fumadocs-ui/components/card"; import { Step, Steps } from "fumadocs-ui/components/steps"; ## Simple S3 Uploads, Zero Vendor Lock-in Upload files directly to S3-compatible storage. Lightweight (6KB), type-safe, and works everywhere. No monthly fees, no vendor lock-in—just **3 files and \~50 lines of code**. ```typescript // Create your upload client const upload = createUploadClient({ endpoint: '/api/upload' }); // Use anywhere in your app export function MyComponent() { const { uploadFiles, files, isUploading } = upload.imageUpload(); return ( uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> ); } ```
## Why Choose Pushduck? **Alternative to UploadThing** - Own your infrastructure, zero recurring costs. | Feature | Pushduck | UploadThing | | ------------------ | -------------------- | ----------------------- | | **Cost** | $0 (use your S3) | $10-25/month | | **Bundle Size** | 6KB | Managed client | | **Vendor Lock-in** | None - S3 compatible | Locked to their service | | **File Ownership** | Your S3 bucket | Their storage | | **Type Safety** | Full TypeScript | TypeScript support | | **Setup Time** | \~2 minutes | \~2 minutes | **Key benefits:** * ✅ **6KB bundle** - No heavy AWS SDK * ✅ **Type-safe** - Compile-time route validation * ✅ **Own your files** - Any S3-compatible provider * ✅ **No monthly fees** - Use your own S3 * ✅ **Focused library** - Does uploads, nothing else ## More Resources
## What's Included * ✅ **Progress Tracking** - Real-time progress, speed, and ETA * ✅ **Type Safety** - Full TypeScript from server to client * ✅ **Multi-Provider** - AWS S3, Cloudflare R2, DigitalOcean, MinIO * ✅ **Validation** - File type, size, and custom rules * ✅ **Storage Operations** - List, delete, and manage files * ✅ **Framework Support** - Next.js, Remix, Express, Fastify, and more * ✅ **Drag & Drop Components** - Copy-paste UI components via CLI 📖 **What we don't do** - File processing, analytics, team management. See [Philosophy](/docs/philosophy) for our focused scope. # Limitations (/docs/limitations) Pushduck aims to be universal, but there are real architectural boundaries. This page is honest about what it **does not** do, so you can scope your project correctly before adopting it. ## Type sharing requires a shared TypeScript codebase Pushduck's typesafe router (`InferClientRouter`) works by importing the router's type from your server file directly into your client. This requires: * Backend and frontend in the **same TypeScript project or monorepo**, or * A **shared package** both sides can import types from, or * Manually exporting and versioning the router type alongside your API **What works:** * Fullstack Next.js / Remix / SvelteKit / Nuxt — backend route and frontend hook live in the same TS project. `InferClientRouter` works out of the box. * Monorepos where `packages/api` and `apps/web` share a TS boundary. * Any setup where you can `import type { AppRouter } from "../server/router"`. **What doesn't work automatically:** * **Separate frontend and backend repositories.** You have two choices: 1. Publish a tiny types-only package from your backend repo and consume it from the frontend. 2. Use the REST contract directly (Pushduck still works end-to-end — you just lose route-name autocomplete and inferred metadata types). * **Git-submodule setups** without a TS path alias between them. ## Non-TypeScript backends are not supported for typesafe routes If your backend is in **Python, Go, Rust, Ruby, PHP, Java, Elixir, or anything else**, Pushduck's server-side API (`createUploadConfig`, `s3.createRouter`, adapters) is not usable. You cannot define routes with a non-TS server. **What you can still do:** * Implement the presigned URL endpoints yourself in your backend language. Pushduck's client is pure fetch/XHR against a documented REST contract, so you can point the client at any server that returns the expected JSON shape. * Use the `createUploadClient` on the frontend with `endpoint: "/api/upload"` pointing to your non-TS backend, and handle the `presign` / `complete` actions manually on the server side. You lose: * Typesafe route names * Automatic metadata inference * The file schema validation built into `s3.image()` / `s3.file()` chains You keep: * XHR-based progress tracking * Multi-file uploads * The presigned URL flow Think of Pushduck as a **TypeScript-first library** in this sense — cross-language support is a REST contract, not a code contract. ## Non-React frontends have no shipped hook `useUploadRoute` and `createUploadClient` currently target React. There is no shipped hook for: * Vue * Svelte * Solid * Angular * Vanilla JS / Web Components **What you can still do:** The underlying logic is in `packages/pushduck/src/client/upload-client.ts` — a small set of functions that presign, upload via XHR, and complete. You can wrap it yourself in any framework's reactive primitive. A Vue composable or a Svelte store would be \~50 lines. Contributions welcome. ## Node server-side streaming uploads are out of scope Pushduck's core upload flow is **client → presigned URL → S3 directly**. The library is not a Node streaming upload helper. If you need to: * Accept a multipart upload in a Node server handler and stream it to S3, or * Upload a file *from* your Node server *to* S3 (e.g. image processing pipelines) you want the native `@aws-sdk/client-s3` `Upload` helper or `aws4fetch` directly. Pushduck's storage API (`s3.put`, `s3.delete`, `s3.list`) supports server-side one-shot operations, but it is not optimized for streaming large files through your Node process. ## No pause, resume, or automatic retry on network interruption Pushduck uses a **single PUT request per file** to a presigned URL. This is the simplest flow S3 offers and it works everywhere, but it has hard limits: * **No pause button.** Once an upload starts, there is no API to suspend it and continue later. The only way to stop is to abort the XHR entirely, which discards all bytes already sent. * **No resume after a dropped connection.** If the network drops mid-upload — Wi-Fi disconnects, the user walks into an elevator, a mobile connection flips to airplane mode — the XHR errors out and the already-uploaded bytes are lost on the server side. The next attempt starts over from byte 0. * **No automatic retry.** Pushduck does not retry failed uploads. If an upload errors, `onError` fires and the file is marked failed. Retry logic is yours to build on top (call `uploadFiles` again with the same file). * **No progress persistence across page reloads.** Refresh the tab mid-upload and the upload state is gone — there is no IndexedDB queue, no background sync worker, no service worker fallback. **Why this is deliberate:** S3 multipart uploads *do* support resumable transfer, but they require orchestrating \~5MB chunks, tracking part numbers, committing/aborting the multipart session, and handling partial-state cleanup when a browser tab dies. That's a different library shape from what Pushduck is — it would roughly double the surface area and complicate the auth/middleware story. **What you can still do:** * For small-to-medium files (under \~100 MB on a decent connection), the single-PUT flow is fine in practice. A dropped upload is a rare event and "try again" is an acceptable UX. * Wrap `uploadFiles` in your own retry logic: catch the error in `onError`, wait with backoff, call `uploadFiles` again. * Show a "your upload was interrupted — tap to retry" UI, since reliable retry requires user intent anyway. **When to look elsewhere:** * If you need true resumable uploads for very large files (larger than 500 MB) on flaky connections — e.g. mobile users uploading video — use `tus-js-client` (resumable protocol with server-side state) or S3 multipart uploads directly via the AWS SDK. * If you need background uploads that survive tab closure — service workers with Background Sync API, or a native app wrapper. ## No multipart uploads — effective file size ceiling Pushduck uploads each file as a **single HTTP PUT to a presigned URL**. S3's multipart upload API (which splits a file into chunks uploaded independently and then committed as one object) is **not implemented**. This puts a practical ceiling on how large a file Pushduck can upload reliably: * **S3 hard limit for a single PUT:** 5 GB. Anything larger is rejected by S3 itself with `EntityTooLarge`. * **Practical limit on mobile / flaky networks:** much lower, often 100–500 MB. Larger single PUTs become increasingly unlikely to complete in one uninterrupted attempt. * **Memory pressure on the client:** on React Native, `fetch(uri).blob()` reads the entire file into memory before uploading. Very large files can OOM the app. Web `File` is streamed by the browser, so desktop is less affected, but still bounded by tab memory. **What multipart uploads would unlock (and what you lose without them):** * Uploading files larger than 5 GB (up to 5 TB, S3's hard ceiling) * Parallel chunk uploads for faster throughput on fast links * Resume-from-last-committed-chunk after a network failure * Lower peak memory because each chunk is uploaded and released independently **What to do right now if you need to upload very large files:** * For files up to a few hundred MB: Pushduck works, but set an explicit `maxFileSize` on your route (`s3.file().maxFileSize("500MB")`) and communicate the limit to users in the UI. * For files larger than that: you'll need to implement S3 multipart yourself using `@aws-sdk/client-s3`'s `Upload` helper on the server, or use a resumable protocol like tus. Pushduck is not the right tool for terabyte-class uploads. Multipart support is on the roadmap but not yet shipped. If this is a blocker for your use case, open an issue — it helps prioritize. ## Service-worker / tab-close uploads are not supported Uploads run in the same JS context that called `uploadFiles`. Closing the tab, navigating away, losing a mobile app to the background (iOS/Android suspend JS execution on app switch or screen lock), or force-quitting the app will cancel the in-flight XHR. Pushduck does not: * Register a service worker to continue uploads in the background * Persist upload queues in IndexedDB across page reloads * Use the Background Fetch or Background Sync APIs If you need uploads that survive tab closure, Pushduck is not the right tool — look at a service-worker-based upload queue or a native app. ## Upload progress requires XHR, not fetch Progress tracking uses `XMLHttpRequest.upload.onprogress`. This means: * Upload progress works in all browsers and React Native (XHR is polyfilled in RN 0.68+) * Upload progress does **not** work in environments without XHR (some edge runtimes, Deno server-side, Node without a polyfill) * You cannot swap the transport to `fetch` without losing progress — `fetch` has no upload progress API on any runtime today This is a deliberate tradeoff. Progress is more valuable than transport flexibility for this library's use case. ## Storage providers are S3-compatible only Pushduck supports **AWS S3, Cloudflare R2, DigitalOcean Spaces, and MinIO** — all of which speak the S3 API. It does **not** support: * Google Cloud Storage (different API surface) * Azure Blob Storage (different API surface) * Backblaze B2 native API (use their S3-compatible endpoint instead) * Local filesystem / on-disk storage If your provider speaks the S3 API, it will probably work with the generic S3 provider config. If it doesn't, Pushduck is not the right tool. ## No built-in authentication or authorization Pushduck ships `middleware` hooks on the router, but the **auth logic itself is yours to write**. It does not: * Ship integrations with Clerk, Auth.js, BetterAuth, Lucia, or any auth library * Verify tokens or sessions on your behalf * Enforce per-user quotas or rate limits Every example in the docs shows `middleware: async ({ req }) => { const user = await yourAuth(req); if (!user) throw new Error("Unauthorized"); return { userId: user.id }; }` — that's the entire auth story. You wire it, Pushduck trusts you. ## No admin dashboard, no hosted service, no managed anything Pushduck is a library, not a product. There is no: * Web dashboard to view uploaded files * Hosted bucket / managed storage * Analytics on upload activity * Billing / quotas / multi-tenant management You bring the bucket, you bring the server, you bring the auth. Pushduck handles the presign dance and the client upload loop. That is the entire product. *** If any of these limitations are blockers for you, please [open an issue](https://github.com/abhay-ramesh/pushduck/issues) — some of them (Vue/Svelte hooks, a non-TS client contract spec, a GCS adapter) are on the roadmap and community input helps prioritize. # Manual Setup (/docs/manual-setup) import { Step, Steps } from "fumadocs-ui/components/steps"; import { Callout } from "fumadocs-ui/components/callout"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; ## Prerequisites * Next.js 13+ with App Router * An S3-compatible storage provider (we recommend Cloudflare R2 for best performance and cost) * Node.js 18+ ## Install Pushduck npm pnpm yarn bun ```bash npm install pushduck ``` ```bash pnpm add pushduck ``` ```bash yarn add pushduck ``` ```bash bun add pushduck ``` ## Set Environment Variables Create a `.env.local` file in your project root with your storage credentials: Cloudflare R2 AWS S3 ```dotenv title=".env.local" # Cloudflare R2 Configuration CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_key CLOUDFLARE_R2_ACCOUNT_ID=your_account_id CLOUDFLARE_R2_BUCKET_NAME=your-bucket-name ``` ```dotenv title=".env.local" # AWS S3 Configuration AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_REGION=us-east-1 AWS_S3_BUCKET_NAME=your-bucket-name ``` **Don't have credentials yet?** Follow our [Provider setup guide](/docs/providers) to create a bucket and get your credentials in 2 minutes. ## Configure Upload Settings First, create your upload configuration: ```typescript // lib/upload.ts import { createUploadConfig } from "pushduck/server"; // Configure your S3-compatible storage export const { s3, storage } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.CLOUDFLARE_R2_ACCESS_KEY_ID!, secretAccessKey: process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY!, region: "auto", endpoint: `https://${process.env.CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, bucket: process.env.CLOUDFLARE_R2_BUCKET_NAME!, accountId: process.env.CLOUDFLARE_R2_ACCOUNT_ID!, }) .build(); ``` ## Create Your Upload Router Create an API route to handle file uploads: ```typescript // app/api/s3-upload/route.ts import { s3 } from "@/lib/upload"; const s3Router = s3.createRouter({ // Define your upload routes with validation imageUpload: s3 .image() .maxFileSize("10MB") .accept(["image/jpeg", "image/png", "image/webp"]), documentUpload: s3.file().maxFileSize("50MB").accept(["application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]), }); export const { GET, POST } = s3Router.handlers; // Export the router type for client-side type safety export type Router = typeof s3Router; ``` **What's happening here?** - `s3.createRouter()` creates a type-safe upload handler - `s3.image()` and `s3.file()` provide validation and TypeScript inference - The router automatically handles presigned URLs, validation, and errors - Exporting the type enables full client-side type safety ## Create Upload Client Create a type-safe client for your components: ```typescript // lib/upload-client.ts import { createUploadClient } from "pushduck"; import type { Router } from "@/app/api/s3-upload/route"; // Create a type-safe upload client export const upload = createUploadClient({ baseUrl: "/api/s3-upload", }); // You can also export specific upload methods export const { imageUpload, documentUpload } = upload; ``` ## Use in Your Components Now you can use the upload client in any component with full type safety: ```typescript // components/image-uploader.tsx "use client"; import { upload } from "@/lib/upload-client"; export function ImageUploader() { const { uploadFiles, uploadedFiles, isUploading, progress, error } = upload.imageUpload(); const handleFileChange = (e: React.ChangeEvent) => { const files = e.target.files; if (files) { uploadFiles(Array.from(files)); } }; return (
{isUploading && (
Uploading... {Math.round(progress)}%
)}
{error && (

{error.message}

)} {uploadedFiles.length > 0 && (
{uploadedFiles.map((file) => (
Uploaded image

{file.name}

))}
)}
); } ``` ## Add to Your Page Finally, use your upload component in any page: ```typescript // app/page.tsx import { ImageUploader } from "@/components/image-uploader"; export default function HomePage() { return (

Upload Images

); } ```
## Setup Complete You now have **production-ready file uploads** working in your Next.js app! Here's what you accomplished: * **Type-safe uploads** with full TypeScript inference * **Automatic validation** for file types and sizes * **Progress tracking** with loading states * **Error handling** with user-friendly messages * **Secure uploads** using presigned URLs * **Multiple file support** with image preview **Turbo Mode Issue:** If you're using `next dev --turbo` and experiencing upload issues, try removing the `--turbo` flag from your dev script. There's a known compatibility issue with Turbo mode that can affect file uploads. ## What's Next? Now that you have the basics working, explore these advanced features:

Enhanced UI

Add drag & drop, progress bars, and beautiful components

Image Upload Guide →
{" "}

Custom Validation

Add authentication, custom metadata, and middleware

Router Configuration →
{" "}

Other Providers

Try Cloudflare R2 for better performance, or AWS S3, DigitalOcean, MinIO

Provider Setup →

Enhanced Client

Upgrade to property-based access for better DX

Migration Guide →
## Need Help? * **Documentation**: Explore our comprehensive [guides](/docs/guides) * **Community**: Join our [Discord community](https://pushduck.dev/discord) * **Issues**: Report bugs on [GitHub](https://github.com/abhay-ramesh/pushduck) * **Support**: Email us at [support@pushduck.com](mailto:support@pushduck.com) **Find Pushduck useful?** Star us on [GitHub](https://github.com/abhay-ramesh/pushduck) and help spread the word! # Philosophy & Scope (/docs/philosophy) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; ## Our Philosophy Pushduck is a **focused upload library**, not a platform. We believe in doing one thing exceptionally well: > The fastest, most lightweight way to add S3 file uploads to any web application This document defines the boundaries of what Pushduck will and won't do, and explains why. *** ## Core Principles ### Lightweight First Bundle size is a feature, not an afterthought. Every dependency is carefully considered. **We use:** * `aws4fetch` (6.4KB) instead of AWS SDK (500KB+) * Native `fetch()` API * Zero unnecessary dependencies **Result:** Core library stays under 10KB minified + gzipped *** ### Focused Scope Do one thing (uploads) exceptionally well, rather than many things poorly. **We believe:** * Specialized tools beat all-in-one solutions * Small, focused libraries are easier to maintain * Users prefer composing tools over vendor lock-in **Result:** You can replace Pushduck easily if needed, or use it alongside other tools *** ### 🔌 Extensibility Over Features Provide hooks and APIs, not built-in everything. **We provide:** * Middleware system for custom logic * Lifecycle hooks for integration points * Type-safe APIs for extension **You implement:** * Your specific business logic * Integration with your services * Custom workflows **Result:** Maximum flexibility without bloat *** ### Document, Don't Implement Show users how to integrate, don't build the integration. **We provide:** * Clear integration patterns * Example code * Best practices documentation **We don't build:** * Database adapters * Auth providers * Email services * Analytics platforms **Result:** Works with any stack, no vendor lock-in *** ## What Pushduck Does ### Core Upload Features Upload files directly to S3 without touching your server. Reduces bandwidth costs and improves performance. Track upload progress, speed, and ETA. Per-file and overall progress metrics for multi-file uploads. Validate file size, type, count, and custom rules. Prevent invalid uploads before they reach S3. Works with AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO, and any S3-compatible provider. ### Storage Operations ```typescript // List files const files = await storage.list.files({ prefix: "uploads/", maxResults: 50 }); // Delete files await storage.delete.file("uploads/old.jpg"); await storage.delete.byPrefix("temp/"); // Get metadata const info = await storage.metadata.getInfo("uploads/doc.pdf"); // Generate download URLs const url = await storage.download.presignedUrl("uploads/file.pdf", 3600); ``` **What we provide:** * ✅ List files with pagination and filtering * ✅ Delete files (single, batch, by prefix) * ✅ Get file metadata (size, date, content-type) * ✅ Generate presigned URLs (upload/download) * ✅ Check file existence **What we don't provide:** * ❌ File search/indexing (use Algolia, Elasticsearch) * ❌ File versioning (use S3 versioning) * ❌ Storage analytics (provide hooks for your analytics) * ❌ Duplicate detection (implement via hooks) *** ### Developer Experience Intelligent type inference from server to client. Catch errors at compile time. Works with Next.js, React, Express, Fastify, and more. Web Standards-based. Interactive setup wizard, automatic provider detection, and project scaffolding. Test your upload flows without hitting real S3. Perfect for CI/CD. *** ### Optional UI Components Following the [shadcn/ui](https://ui.shadcn.com) approach: ```bash # Copy components into your project npx @pushduck/cli add upload-dropzone npx @pushduck/cli add file-list ``` **What we provide:** * ✅ Basic upload UI components (dropzone, file-list, progress-bar) * ✅ Headless/unstyled components you can customize * ✅ Copy-paste, not installed as dependency **What we don't provide:** * ❌ Full-featured file manager UI * ❌ Image gallery/carousel components * ❌ File preview modals * ❌ Admin dashboard components **Philosophy:** You own the code, you customize it. We provide starting points, not rigid components. *** ## What Pushduck Doesn't Do ### File Processing **Out of Scope** - Use specialized tools for these tasks **We don't process files. Use these instead:** | Task | Recommended Tool | Why | | --------------------- | -------------------------------------------------- | ---------------------------- | | Image optimization | [Sharp](https://sharp.pixelplumbing.com/) | Best-in-class, battle-tested | | Video transcoding | [FFmpeg](https://ffmpeg.org/) | Industry standard | | PDF generation | [PDFKit](https://pdfkit.org/) | Specialized library | | Image transformations | [Cloudflare Images](https://cloudflare.com/images) | Edge-optimized | | Content moderation | AWS Rekognition, Cloudflare | Purpose-built services | **Why not?** * These tools do it better than we ever could * Adding them would balloon our bundle size * Creates unnecessary dependencies * Limits user choice **Our approach:** Document integration patterns **⚠️ Bandwidth Note:** Server-side processing requires downloading from S3 (inbound) and uploading variants (outbound). This negates the "server never touches files" benefit. **Better options:** * **Client-side preprocessing** (before upload) - Zero server bandwidth * **URL-based transforms** (Cloudinary, Imgix) - Zero server bandwidth * See [Image Uploads Guide](/docs/guides/image-uploads) for detailed patterns ```typescript // Example: Integrate with Sharp import sharp from 'sharp'; const router = s3.createRouter({ imageUpload: s3.image() .onUploadComplete(async ({ key }) => { // ⚠️ Downloads file from S3 to server const buffer = await s3.download(key); // Process with Sharp const optimized = await sharp(buffer) .resize(800, 600) .webp({ quality: 80 }) .toBuffer(); // ⚠️ Uploads processed file back to S3 await storage.upload.file(optimized, `optimized/${key}`); }) }); ``` ```typescript // ✅ Better: Client-side preprocessing (recommended) import imageCompression from 'browser-image-compression'; function ImageUpload() { const { uploadFiles } = upload.images(); const handleUpload = async (file: File) => { // ✅ Compress on client BEFORE upload const compressed = await imageCompression(file, { maxSizeMB: 1, maxWidthOrHeight: 1920, }); // Upload already-optimized file await uploadFiles([compressed]); }; } ``` *** ### Backend Services **Integration Pattern** - We provide hooks, you connect services **We don't implement these services:** | Service | What We Provide | You Implement | | --------------- | ----------------------- | ------------------ | | Webhooks | Lifecycle hooks | Webhook delivery | | Notifications | `onUploadComplete` hook | Email/SMS sending | | Database | File metadata in hooks | DB storage logic | | Queue Systems | Hooks with context | Queue integration | | Background Jobs | Async hook support | Job processing | | Analytics | Hooks with event data | Analytics tracking | **Example Integration:** ```typescript import { db } from '@/lib/database'; const router = s3.createRouter({ fileUpload: s3.file() .onUploadComplete(async ({ file, key, url, metadata }) => { // You implement database logic await db.files.create({ data: { name: file.name, size: file.size, url: url, s3Key: key, userId: metadata.userId, uploadedAt: new Date() } }); }) }); ``` ```typescript import { sendWebhook } from '@/lib/webhooks'; const router = s3.createRouter({ fileUpload: s3.file() .onUploadComplete(async ({ file, url }) => { // You implement webhook delivery await sendWebhook({ event: 'file.uploaded', data: { filename: file.name, url: url, timestamp: new Date().toISOString() } }); }) }); ``` ```typescript import { sendEmail } from '@/lib/email'; const router = s3.createRouter({ fileUpload: s3.file() .onUploadComplete(async ({ file, metadata }) => { // You implement email notifications await sendEmail({ to: metadata.userEmail, subject: 'File Upload Complete', body: `Your file "${file.name}" has been uploaded successfully.` }); }) }); ``` ```typescript import { queue } from '@/lib/queue'; const router = s3.createRouter({ fileUpload: s3.file() .onUploadComplete(async ({ file, key }) => { // You implement queue integration await queue.add('process-file', { fileKey: key, fileName: file.name, processType: 'thumbnail-generation' }); }) }); ``` ```typescript import { analytics } from '@/lib/analytics'; const router = s3.createRouter({ fileUpload: s3.file() .onUploadStart(async ({ file, metadata }) => { // Track upload start await analytics.track('upload_started', { userId: metadata.userId, fileSize: file.size, fileType: file.type }); }) .onUploadComplete(async ({ file, url, metadata }) => { // Track successful upload await analytics.track('upload_completed', { userId: metadata.userId, fileName: file.name, fileSize: file.size, fileUrl: url }); }) .onUploadError(async ({ error, metadata }) => { // Track errors await analytics.track('upload_failed', { userId: metadata.userId, error: error.message }); }) }); ``` **Why this approach?** * ✅ You're not locked into our choice of services * ✅ Use your existing infrastructure * ✅ Switch services without changing upload library * ✅ Keeps our bundle size minimal *** ### Platform Features **Not a Platform** - Pushduck is a library, not a SaaS **We will never build:** ❌ **User Management** - Use NextAuth, Clerk, Supabase Auth, etc.\ ❌ **Team/Organization Systems** - Build in your application\ ❌ **Permission/Role Management** - Implement in your middleware\ ❌ **Analytics Dashboards** - We provide hooks for your analytics\ ❌ **Admin Panels** - Build with your UI framework\ ❌ **Billing/Subscriptions** - Use Stripe, Paddle, etc.\ ❌ **API Key Management** - Implement in your system\ ❌ **Audit Logs** - Log via hooks to your logging service **Why not?** * Every app has different requirements * Would require a backend service (we're a library) * Creates vendor lock-in * Massive scope creep from our core mission **Our approach:** Provide middleware hooks ```typescript import { auth } from '@/lib/auth'; import { checkPermission } from '@/lib/permissions'; import { logAudit } from '@/lib/audit'; const router = s3.createRouter({ fileUpload: s3.file() .middleware(async ({ req, metadata }) => { // YOUR auth system const user = await auth.getUser(req); if (!user) throw new Error('Unauthorized'); // YOUR permissions system if (!checkPermission(user, 'upload:create')) { throw new Error('Forbidden'); } // YOUR audit logging await logAudit({ userId: user.id, action: 'file.upload.started', metadata: metadata }); return { userId: user.id }; }) }); ``` *** ### Authentication & Authorization **What we provide:** * ✅ Middleware hooks for auth checks * ✅ Access to request context (headers, cookies, etc.) * ✅ Integration examples with popular auth providers **What we don't provide:** * ❌ Built-in auth system * ❌ Session management * ❌ OAuth providers * ❌ API key generation * ❌ User database **Example Integrations:** ```typescript import { auth } from '@/lib/auth'; const router = s3.createRouter({ fileUpload: s3.file() .middleware(async ({ req }) => { const session = await auth.api.getSession({ headers: req.headers }); if (!session?.user) { throw new Error('Please sign in to upload files'); } return { userId: session.user.id, userEmail: session.user.email }; }) }); ``` ```typescript import { getServerSession } from 'next-auth'; const router = s3.createRouter({ fileUpload: s3.file() .middleware(async ({ req }) => { const session = await getServerSession(); if (!session?.user) { throw new Error('Please sign in to upload files'); } return { userId: session.user.id, userEmail: session.user.email }; }) }); ``` ```typescript import { auth } from '@clerk/nextjs'; const router = s3.createRouter({ fileUpload: s3.file() .middleware(async () => { const { userId } = auth(); if (!userId) { throw new Error('Unauthorized'); } return { userId }; }) }); ``` ```typescript import { createServerClient } from '@supabase/ssr'; const router = s3.createRouter({ fileUpload: s3.file() .middleware(async ({ req }) => { const supabase = createServerClient(/* config */); const { data: { user } } = await supabase.auth.getUser(); if (!user) { throw new Error('Unauthorized'); } return { userId: user.id }; }) }); ``` ```typescript import { verifyToken } from '@/lib/auth'; const router = s3.createRouter({ fileUpload: s3.file() .middleware(async ({ req }) => { const token = req.headers.get('authorization')?.replace('Bearer ', ''); if (!token) { throw new Error('No token provided'); } const user = await verifyToken(token); if (!user) { throw new Error('Invalid token'); } return { userId: user.id }; }) }); ``` *** ## The Integration Pattern This is our core philosophy in action: ```typescript // 1. We handle uploads // 2. You connect your services via hooks // 3. Everyone wins const router = s3.createRouter({ fileUpload: s3.file() // YOUR auth .middleware(async ({ req }) => { const user = await yourAuth.getUser(req); return { userId: user.id }; }) // YOUR business logic .onUploadStart(async ({ file, metadata }) => { await yourAnalytics.track('upload_started', { userId: metadata.userId, fileSize: file.size }); }) // YOUR database .onUploadComplete(async ({ file, url, key, metadata }) => { await yourDatabase.files.create({ userId: metadata.userId, url: url, s3Key: key, name: file.name, size: file.size }); // YOUR notifications await yourEmailService.send({ to: metadata.userEmail, template: 'upload-complete', data: { fileName: file.name } }); // YOUR webhooks await yourWebhooks.trigger({ event: 'file.uploaded', data: { url, fileName: file.name } }); // YOUR queue await yourQueue.add('process-file', { fileKey: key, userId: metadata.userId }); // YOUR analytics await yourAnalytics.track('upload_completed', { userId: metadata.userId, fileName: file.name, fileSize: file.size, fileUrl: url, fileKey: key }); }) // YOUR error handling .onUploadError(async ({ error, metadata }) => { await yourErrorTracking.log({ error: error, userId: metadata.userId }); }) }); ``` **Benefits:** * 🪶 Pushduck stays lightweight (only upload logic) * 🔌 You use your preferred services * 🎯 No vendor lock-in * ⚡ No unnecessary code in your bundle * 🔧 Maximum flexibility *** ## 🤔 Decision Framework When considering new features, we ask: ### Add if: 1. **Core to uploads** - Directly helps files get to S3 2. **Universally needed** - 80%+ of users need it 3. **Can't be solved externally** - Must be part of upload flow 4. **Lightweight** - Doesn't balloon bundle size 5. **Framework agnostic** - Works everywhere ### Don't add if: 1. **Better tools exist** - Sharp does image processing better 2. **Service-specific** - Requires backend infrastructure 3. **Opinion-heavy** - Database choice, auth provider, etc. 4. **UI-specific** - Every app needs different UI 5. **Platform feature** - User management, billing, etc. ### 🔌 Provide hooks if: 1. **Common integration point** - Many users need it 2. **Can be external** - Services can be swapped 3. **Timing matters** - Needs to happen at specific point in upload lifecycle *** ## What This Means For You ### As a User **You get:** * ✅ Lightweight, focused upload library * ✅ Freedom to choose your own tools * ✅ No vendor lock-in * ✅ Clear integration patterns * ✅ Stable, predictable API **You're responsible for:** * 🔧 Choosing and integrating your services * 🔧 Building your UI (or copy ours) * 🔧 Implementing your business logic * 🔧 Managing your infrastructure ### As a Contributor **Focus contributions on:** * ✅ Core upload features (resumable, queuing, etc.) * ✅ Framework adapters * ✅ Testing utilities * ✅ Documentation & examples * ✅ Integration guides **We'll reject PRs for:** * ❌ File processing features * ❌ Backend services (webhooks, notifications) * ❌ Database adapters * ❌ Auth providers * ❌ Platform features *** ## Further Reading Our development roadmap and planned features Guidelines for contributing to the project Patterns for integrating databases, auth, notifications, and more Complete examples showing integration patterns *** ## 💬 Questions? Have questions about scope or philosophy? * 💭 [GitHub Discussions](https://github.com/abhay-ramesh/pushduck/discussions) * 💬 [Discord Community](https://pushduck.dev/discord) * 🐛 [GitHub Issues](https://github.com/abhay-ramesh/pushduck/issues) **Remember:** We're focused on being the best upload library, not the biggest. Every feature we say "no" to keeps Pushduck fast, lightweight, and maintainable. # Quick Start (/docs/quick-start) import { Step, Steps } from "fumadocs-ui/components/steps"; import { Callout } from "fumadocs-ui/components/callout"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Accordion, Accordions } from "fumadocs-ui/components/accordion"; Get file uploads working in **3 simple steps**. No overwhelming configuration, just the essentials. **Prefer automated setup?** Use `npx @pushduck/cli init` for zero-config setup. This guide is for manual installation. ### Install Pushduck npm pnpm yarn bun ```bash npm install pushduck ``` ```bash pnpm add pushduck ``` ```bash yarn add pushduck ``` ```bash bun add pushduck ``` ### Create API Route One file with your S3 config and upload route: ```ts title="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; ``` **Environment variables**: Add your S3 credentials to `.env.local`. See [Provider Setup](/docs/providers) for getting credentials. **File paths**: By default, files are uploaded with just the filename (e.g., `photo.jpg`). To organize files into folders, see [Path Configuration](/docs/api/configuration/path-configuration). ### Create Client & Use in Components Create a reusable client and use it in your components: ```ts title="lib/upload-client.ts" import { createUploadClient } from 'pushduck/client'; import type { AppRouter } from '@/app/api/upload/route'; export const upload = createUploadClient({ endpoint: '/api/upload' }); ``` ```tsx title="app/upload-demo.tsx" 'use client'; import { upload } from '@/lib/upload-client'; export function UploadDemo() { const { uploadFiles, files, isUploading } = upload.imageUpload(); return (
uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> {files.map((file) => (
{file.name} - {file.progress}% {file.status === 'success' && {file.name}}
))}
); } ```
## Done! That's it - **3 steps, 3 files, \~40 lines of code**, and you have production-ready file uploads! *** ## Need More? For production apps, you'll want to add authentication and custom paths: ```ts title="app/api/upload/route.ts" const { s3 } = createUploadConfig() .provider("aws", { /* ... */ }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => `${metadata.userId}/${Date.now()}/${file.name}` }) .build(); const router = s3.createRouter({ imageUpload: s3.image() .maxFileSize('5MB') .middleware(async ({ req }) => { const user = await getUser(req); if (!user) throw new Error('Unauthorized'); return { userId: user.id }; }), }); ``` See [Configuration Guide](/docs/api/configuration/upload-config) for all options. Using Remix, SvelteKit, Hono, or another framework? See the [Integrations](/docs/integrations) page for framework-specific examples. Configure CORS on your S3 bucket: ```json [ { "AllowedOrigins": ["http://localhost:3000", "https://yourdomain.com"], "AllowedMethods": ["GET", "PUT", "POST", "DELETE"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"] } ] ``` See [Provider Setup](/docs/providers) for detailed CORS configuration. Want drag & drop, progress bars, and styled components? ```bash npx @pushduck/cli add upload-dropzone npx @pushduck/cli add file-list ``` Or see our [Examples](/docs/examples) for production-ready components. ## Next Steps # Roadmap (/docs/roadmap) import { Callout } from "fumadocs-ui/components/callout"; ## How to read this roadmap Pushduck's roadmap is structured by **intent**, not by quarter. Dates rot; intent doesn't. * **Shipped** — in the current version, works today * **In progress** — actively being touched right now * **Next** — committed, near-term, not yet started * **Later** — aspirational, meaningful work, no commitment to dates * **Under consideration** — not committed, driven by demand * **Not doing** — explicit out-of-scope, belongs in your code or in a recipe See [Philosophy](/docs/philosophy) for why the scope is what it is. See [Limitations](/docs/limitations) for the honest list of what the current version doesn't do. *** ## Shipped ### Core * **Web-standard `Request`/`Response` handler** — works on any runtime that speaks WinterCG. Next.js, Hono, Elysia, Bun, Deno, SvelteKit, Nuxt, Remix, Astro, Cloudflare Workers, Vercel edge, Netlify functions, etc., work without a dedicated adapter. * **Typesafe router** with `InferClientRouter` — server and client share types in a TS monorepo. * **Middleware chain** with typed metadata flowing through. * **Lifecycle hooks**: `onStart`, `onProgress`, `onSuccess`, `onError`, `onUploadComplete`. * **Schema chain**: `s3.image()`, `s3.file()`, `.accept()`, `.maxFileSize()`, `.maxFiles()`, `.middleware()`, `.paths()`, `.onUploadComplete()`. * **Per-file and aggregate progress tracking** — progress %, upload speed, ETA. * **Client-side file metadata passthrough** — attach app-specific metadata on `uploadFiles(files, metadata)` and receive it in server middleware and lifecycle hooks. ### Providers * **AWS S3** * **Cloudflare R2** * **DigitalOcean Spaces** * **MinIO** (self-hosted, dev, testing) * Any other S3-compatible provider via the generic AWS config with a custom endpoint (Backblaze B2, Wasabi, Scaleway, Linode, Storj, Tebi, etc.) ### Framework adapters * **Next.js** (App Router + Pages Router) * **Express** (via `toExpressHandler()`) * **Fastify** (via `toFastifyHandler()`) * Any framework that speaks Web-standard `Request`/`Response` works out of the box without a shim. ### Clients * **`pushduck/client`** — React hook (`useUploadRoute`) with property-based client (`createUploadClient`). * **`pushduck/react-native`** — React Native / Expo with direct picker-asset support. Pass assets from `expo-image-picker`, `expo-document-picker`, or `react-native-image-picker` directly; no field mapping. *** ## In progress * **Documentation and scope alignment** — cleaning up the docs site, adding the [Limitations](/docs/limitations) page, aligning the roadmap with what's actually shipped and planned. * **v1.0 API stabilization** — locking down the public API surface for semver stability ahead of v1.0. *** ## Next These items are committed. They will ship, and they're the focus after the current in-progress work lands. ### Security and correctness Before anything else. These are small, high-value fixes surfaced by code review. * **Verify `Content-Length` is signed** in presigned PUT URLs. If not, enforce server-side size limits via header signing so client-declared sizes can't be spoofed. * **Error-to-status-code mapping** in the universal handler. Thrown middleware errors should return `401`/`403`/`400` with the right shape, not always `500`. * **Body-size limit on `/presign`** — closes a DoS vector where a hostile client POSTs a huge JSON metadata blob. * **Replace `console.error` with the structured logger** in the universal handler. * **Integration tests against MinIO in CI** — end-to-end presign → PUT → complete against a real S3-compatible server in Docker. Catches signing regressions unit tests miss. ### Recipes library A first-party collection of copy-paste snippets for common integrations. Lives at `docs/content/docs/recipes/` and renders as its own section in the sidebar. Planned first set: * `recipes/nextjs-starter` — minimal App Router setup with typed route + client hook * `recipes/clerk-middleware` — auth middleware using Clerk * `recipes/authjs-middleware` — auth middleware using Auth.js * `recipes/betterauth-middleware` — auth middleware using BetterAuth * `recipes/sharp-thumbnails` — thumbnail generation in `onUploadComplete` * `recipes/exif-stripping` — strip GPS/metadata from photos before storing * `recipes/clamav-scan` — virus scanning via ClamAV * `recipes/rate-limiting` — per-user upload quota in middleware * `recipes/db-record` — write an upload row to Postgres / SQLite / Drizzle * `recipes/retry-on-failure` — wrap `uploadFiles` with exponential backoff Recipes are standalone, framework-agnostic, and work without any CLI. ### ShadCN-style CLI `pushduck add ` — a thin command that downloads a recipe from the repo and writes it into the user's codebase. The CLI does almost nothing; the recipes do the work. ```bash npx pushduck add nextjs-starter npx pushduck add clerk-middleware npx pushduck add sharp-thumbnails ``` **Design principles for the CLI:** * No framework detection magic — the user picks the recipe, the CLI copies the file * No installing extra packages — each recipe is self-contained TypeScript that drops into the codebase * No rewriting existing files — the CLI only creates new files and prompts the user to wire them up * Tiny: \~100 lines, no detection logic, no AST manipulation This is the ShadCN CLI pattern applied to upload recipes. The previous CLI attempt was fragile because it tried to auto-detect the user's framework and edit their config files. This version doesn't do any of that. ### Framework-agnostic core Extract a pure-JS upload state store (\~100-200 lines, no React, no dependencies) from the current React hook. Once extracted, each framework binding becomes a thin (\~50 line) wrapper subscribing to the same store. This is a prerequisite for Vue and Svelte support. ### Vue support **`pushduck/vue`** — Composition API composable (`useUploadRoute`) with the same typesafe router and progress semantics as the React hook. Targets Vue 3 Composition API and pairs naturally with Nuxt. ### Svelte support **`pushduck/svelte`** — Svelte store wrapping the same core. Works with SvelteKit out of the box; compatible with Svelte 4 stores and Svelte 5 runes. *** ## Later Meaningful work, aspirational timeline, no hard commitment. ### Multipart uploads Support for files larger than 5 GB (S3's single-PUT limit). Uses the S3 multipart upload API — client-side chunking, parallel part uploads, server-side session tracking, commit/abort logic. This is a substantial feature. It roughly doubles the library's surface area — part orchestration, session state, client-side resume tokens, partial-state cleanup when a browser tab dies. It's a major-version effort, not a weekend add. ### Resumable uploads Once multipart lands, resumable uploads come naturally as a follow-up. The client tracks which parts have committed and can resume from the last committed part after a network failure. May adopt the tus protocol or build on top of S3 multipart directly. Together with multipart, this is the biggest gap the library has compared to heavier alternatives. ### Solid support **`pushduck/solid`** — signal-based primitives. Drops out of the framework-agnostic core once that's extracted, so the work is \~a day once the core is ready. ### Vanilla JS / Web Components Raw subscribe-based API without any framework runtime. Targets embedding in any page without React/Vue/Svelte — smallest surface area once the core is extracted. ### GCS (native) Google Cloud Storage via GCS's native REST API, not the S3 interop layer. The S3 interop already works for basic operations, but a native adapter unlocks GCS-specific features (object holds, retention policies, resumable session URIs) that interop doesn't expose. ### Vercel Blob Vercel's storage service — new, growing in the Vercel ecosystem, fits the edge-first narrative. Thin adapter over the Vercel Blob API. ### Service-worker background uploads Survive tab close and mobile backgrounding. Separate service worker with IndexedDB-backed queue. Significant work and a different mental model from the main upload flow; shipped only if enough users hit the "my upload died when I switched apps" problem. ### Path-based action routing Replace `?action=presign` / `?action=complete` query-param routing with `/api/upload/{route}/presign` path segments. Better for CDN caching, observability, and CORS. Breaking change — held for v1.0 or v2.0. *** ## Under consideration Not committed. Prioritized based on user demand. * **Azure Blob Storage** — different API surface from S3 (SAS tokens, different signing). Large enterprise market. Ships if demand is real. * **Supabase Storage** — S3 under the hood with a REST wrapper. May already work via the MinIO/S3 generic config depending on endpoint — needs verification. If not, a thin Supabase adapter. * **Filesystem / "dev-local" provider** — "upload straight to a folder on my server" breaks the client → presigned URL → storage architecture entirely. **The current answer is: run MinIO in Docker** — it's an S3-compatible server that stores to disk, works with Pushduck today, and takes 30 seconds to start (`docker run minio/minio server /data`). A native filesystem provider would ship only if the MinIO path genuinely doesn't fit real use cases. * **Angular support** — different paradigm, non-trivial effort. Not currently planned unless demand materializes. * **Backblaze B2 native** — B2's S3-compatible endpoint already works today. Only add value if the S3 endpoint lacks something users need. * **iCloud / cross-provider mirroring** — out of scope for the library, but could be a recipe if users build it. *** ## Not doing These belong in **your code** (or in a [recipe](#recipes-library)), not in the library. The same scope discipline that keeps Pushduck a thin library also keeps it from turning into a platform. * **File processing** — thumbnail generation, EXIF stripping, format conversion, resizing. Use Sharp, FFmpeg, Jimp, or ImageMagick in your `onUploadComplete` handler. Recipes provided. * **Virus / malware scanning** — use ClamAV, VirusTotal, or a hosted scanner in `onUploadComplete`. Recipe provided. * **Authentication integrations** — `middleware` hook accepts any async function. Recipes provided for Clerk, Auth.js, BetterAuth, Lucia. * **Admin dashboard** — Pushduck is a library, not a platform. * **Hosted service / managed storage** — you bring the bucket. * **Team management, billing, multi-tenant quotas** — application concerns. * **GraphQL integration** — out of scope. Upload mechanics are REST; use GraphQL above the upload layer for metadata. * **Webhooks** — write them in your `onUploadComplete` handler. Recipe provided. * **Built-in analytics** — surface lifecycle events; you wire Plausible, PostHog, Segment, or whatever. * **Built-in upload UI components** — headless state, yes. Styled components, no. Recipes provide examples you can copy and restyle. *** ## What's changed since the previous roadmap * **React Native support shipped** (v0.5.0) — `pushduck/react-native` with direct picker-asset support from `expo-image-picker`, `expo-document-picker`, and `react-native-image-picker`. * **Schema unification** (v0.6.0) — `accept()` replaces the older separate `types`, `formats`, and `extensions` methods into a single call. * **Chain order flexibility** — `maxFileSize`, `maxFiles`, `accept` can be chained in any order on schemas. * **ACL header signing fix** — presigned URLs now sign only the ACL header as required, not the full header set. * **Middleware request type** — `middleware({ req: Request })` is properly typed with the Web-standard `Request`, not `any`. * **Nullish coalescing** for provider config defaults — prevents `0` and empty strings from being silently overridden. *** ## How to influence the roadmap The "Under consideration" and "Later" sections change based on what users actually need. If your use case is blocked by something in those lists: * [Propose it in Ideas](https://github.com/abhay-ramesh/pushduck/discussions/categories/ideas) — or upvote an existing post if someone already asked. Upvotes are how I decide what to promote from "Later" to "Next". * [Ask in Q\&A](https://github.com/abhay-ramesh/pushduck/discussions/categories/q-a) if you need help with something that already exists but isn't working for you. * [Open an issue](https://github.com/abhay-ramesh/pushduck/issues) for bugs or for feature work that's already been committed to. Priority goes to concrete use cases, not speculative requests. "I'm shipping a video upload feature and need multipart for files larger than 5 GB" is more actionable than "it would be nice to have multipart someday." ## Principles As features land, these don't get compromised: * **Small bundle** — every KB added to the client bundle is justified * **Focused scope** — do one thing (S3 uploads) exceptionally well; hooks for everything else * **Type safety** — every new feature has full TypeScript inference * **Backward compatibility** — no gratuitous breaking changes; when they're necessary, they come with a migration guide * **Universal by default** — features work on Workers, Bun, Deno, and Node without special cases * **Unopinionated** — you control auth, processing, persistence, and UX; Pushduck owns the transport and signing # CLI Reference (/docs/api/cli) import { Callout } from 'fumadocs-ui/components/callout' import { Card, Cards } from 'fumadocs-ui/components/card' import { Steps, Step } from 'fumadocs-ui/components/steps' import { Tab, Tabs } from 'fumadocs-ui/components/tabs' import { Files, Folder, File } from 'fumadocs-ui/components/files' **🚀 Recommended**: Use our CLI for the fastest setup experience **Next.js Only**: The pushduck CLI currently only supports Next.js projects. Support for other frameworks is coming soon. ## Quick Start Get your file uploads working in under 2 minutes with our interactive CLI tool. npm pnpm yarn bun ```bash npx @pushduck/cli@latest init ``` ```bash pnpm dlx @pushduck/cli@latest init ``` ```bash yarn dlx @pushduck/cli@latest init ``` ```bash bun x @pushduck/cli@latest init ``` The CLI will automatically: * 🔍 **Detect your package manager** (npm, pnpm, yarn, bun) * 📦 **Install dependencies** using your preferred package manager * ☁️ **Set up your storage provider** (Cloudflare R2, AWS S3, etc.) * 🛠️ **Generate type-safe code** (API routes, client, components) * ⚙️ **Configure environment** variables and bucket setup ## What the CLI Does * Detects App Router vs Pages Router * Finds existing TypeScript configuration * Checks for existing upload implementations * Validates project structure * AWS S3, Cloudflare R2, DigitalOcean Spaces * Google Cloud Storage, MinIO * Automatic bucket creation * CORS configuration * Type-safe API routes * Upload client configuration * Example components * Environment variable templates The CLI walks you through each step, asking only what's necessary for your specific setup. ## CLI Commands ### `init` - Initialize Setup npm pnpm yarn bun ```bash npx @pushduck/cli@latest init [options] ``` ```bash pnpm dlx @pushduck/cli@latest init [options] ``` ```bash yarn dlx @pushduck/cli@latest init [options] ``` ```bash bun x @pushduck/cli@latest init [options] ``` **Options:** * `--provider ` - Skip provider selection (aws|cloudflare-r2|digitalocean|minio|gcs) * `--skip-examples` - Don't generate example components * `--skip-bucket` - Don't create S3 bucket automatically * `--api-path ` - Custom API route path (default: `/api/upload`) * `--dry-run` - Show what would be created without creating * `--verbose` - Show detailed output **Examples:** Quick Setup AWS Direct Custom API Path Components Only ```bash # Interactive setup with all prompts npx @pushduck/cli@latest init ``` ```bash # Skip provider selection, use AWS S3 npx @pushduck/cli@latest init --provider aws ``` ```bash # Use custom API route path npx @pushduck/cli@latest init --api-path /api/files ``` ```bash # Generate only components, skip bucket creation npx @pushduck/cli@latest init --skip-bucket --skip-examples ``` ### `add` - Add Upload Route ```bash npx @pushduck/cli@latest add ``` Add new upload routes to existing configuration: ```bash # Interactive route builder npx @pushduck/cli@latest add # Example output: # Added imageUpload route for profile pictures # Added documentUpload route for file attachments # Updated router types ``` ### `test` - Test Configuration ```bash npx @pushduck/cli@latest test [options] ``` **Options:** * `--verbose` - Show detailed test output Validates your current setup: ```bash npx @pushduck/cli@latest test # Example output: # ✅ Environment variables configured # ✅ S3 bucket accessible # ✅ CORS configuration valid # ✅ API routes responding # ✅ Types generated correctly ``` ## Interactive Setup Walkthrough ### Step 1: Project Detection ``` 🔍 Detecting your project... ✓ Next.js App Router detected ✓ TypeScript configuration found ✓ Package manager: pnpm detected ✓ No existing upload configuration ✓ Project structure validated ``` ### Step 2: Provider Selection ``` ? Which cloud storage provider would you like to use? ❯ Cloudflare R2 (recommended) AWS S3 (classic, widely supported) DigitalOcean Spaces (simple, affordable) Google Cloud Storage (enterprise-grade) MinIO (self-hosted, open source) Custom S3-compatible endpoint ``` ### Step 3: Credential Setup ``` Setting up Cloudflare R2... 🔍 Checking for existing credentials... ✓ Found CLOUDFLARE_R2_ACCESS_KEY_ID ✓ Found CLOUDFLARE_R2_SECRET_ACCESS_KEY ✓ Found CLOUDFLARE_R2_ACCOUNT_ID ⚠ CLOUDFLARE_R2_BUCKET_NAME not found ? Enter your R2 bucket name: my-app-uploads ? Create bucket automatically? Yes ``` ### Step 4: API Configuration ``` ? Where should we create the upload API? ❯ app/api/upload/route.ts (recommended) app/api/s3-upload/route.ts (classic) Custom path ? Generate example upload page? ❯ Yes, create app/upload/page.tsx with full example Yes, just add components to components/ui/ No, I'll build my own ``` ### Step 5: File Generation ``` Generating files... Created files: ├── app/api/upload/route.ts ├── app/upload/page.tsx ├── components/ui/upload-button.tsx ├── components/ui/upload-dropzone.tsx ├── lib/upload-client.ts └── .env.example Installing dependencies... ✓ pushduck ✓ react-dropzone Setup complete! Your uploads are ready. ``` ## Generated Project Structure After running the CLI, your project will have: ### Generated API Route ```typescript title="app/api/upload/route.ts" // No longer needed - use uploadRouter.handlers directly import { s3 } from '@/lib/upload' import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' const s3Router = s3.createRouter({ // Image uploads for profile pictures imageUpload: s3.image() .maxFileSize("5MB") .maxFiles(1) .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ req, metadata }) => { const session = await getServerSession(authOptions) if (!session?.user?.id) { throw new Error("Authentication required") } return { ...metadata, userId: session.user.id, folder: `uploads/${session.user.id}` } }), // Document uploads documentUpload: s3.file() .maxFileSize("10MB") .maxFiles(5) .accept(["application/pdf", "text/plain", "application/msword"]) .middleware(async ({ req, metadata }) => { const session = await getServerSession(authOptions) if (!session?.user?.id) { throw new Error("Authentication required") } return { ...metadata, userId: session.user.id, folder: `documents/${session.user.id}` } }) }) export type AppRouter = typeof s3Router export const { GET, POST } = s3Router.handlers ``` ### Generated Upload Client ```typescript title="lib/upload-client.ts" import { createUploadClient } from 'pushduck/client' import type { AppRouter } from '@/app/api/upload/route' export const upload = createUploadClient({ endpoint: '/api/upload' }) ``` ### Generated Example Page ```typescript title="app/upload/page.tsx" import { UploadButton } from '@/components/ui/upload-button' import { UploadDropzone } from '@/components/ui/upload-dropzone' export default function UploadPage() { return (

File Upload Demo

Profile Picture

Documents

) } ``` ## Environment Variables The CLI automatically creates `.env.example` and prompts for missing values: ```bash title=".env.example" # Cloudflare R2 Configuration (Recommended) CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key_here CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_key_here CLOUDFLARE_R2_ACCOUNT_ID=your_account_id_here CLOUDFLARE_R2_BUCKET_NAME=your-bucket-name # Alternative: AWS S3 Configuration # AWS_ACCESS_KEY_ID=your_access_key_here # AWS_SECRET_ACCESS_KEY=your_secret_key_here # AWS_REGION=us-east-1 # AWS_S3_BUCKET_NAME=your-bucket-name # Next.js Configuration NEXTAUTH_SECRET=your_nextauth_secret_here NEXTAUTH_URL=http://localhost:3000 # Optional: Custom S3 endpoint (for MinIO, etc.) # S3_ENDPOINT=https://your-custom-endpoint.com ``` ## Provider-Specific Setup ```bash npx @pushduck/cli@latest init --provider cloudflare-r2 ``` **What gets configured:** * Cloudflare R2 S3-compatible endpoints * Global edge network optimization * Zero egress fee configuration * CORS settings for web uploads ```bash npx @pushduck/cli@latest init --provider aws ``` **What gets configured:** * AWS S3 regional endpoints * IAM permissions and policies * Bucket lifecycle management * CloudFront CDN integration (optional) ```bash npx @pushduck/cli@latest init --provider digitalocean ``` **Required Environment Variables:** * `AWS_ACCESS_KEY_ID` (DO Spaces key) * `AWS_SECRET_ACCESS_KEY` (DO Spaces secret) * `AWS_REGION` (DO region) * `AWS_S3_BUCKET_NAME` * `S3_ENDPOINT` (DO Spaces endpoint) **What the CLI does:** * Configures DigitalOcean Spaces endpoints * Sets up CDN configuration * Validates access permissions * Configures CORS policies ```bash npx @pushduck/cli@latest init --provider minio ``` **Required Environment Variables:** * `AWS_ACCESS_KEY_ID` (MinIO access key) * `AWS_SECRET_ACCESS_KEY` (MinIO secret key) * `AWS_REGION=us-east-1` * `AWS_S3_BUCKET_NAME` * `S3_ENDPOINT` (MinIO server URL) **What the CLI does:** * Configures self-hosted MinIO endpoints * Sets up bucket policies * Validates server connectivity * Configures development-friendly settings ## Troubleshooting ### CLI Not Found ```bash # If you get "command not found" npm install -g pushduck # Or use npx for one-time usage npx @pushduck/cli@latest@latest init ``` ### Permission Errors ```bash # If you get permission errors during setup sudo npx @pushduck/cli@latest init # Or fix npm permissions npm config set prefix ~/.npm-global export PATH=~/.npm-global/bin:$PATH ``` ### Existing Configuration ```bash # Force overwrite existing configuration npx @pushduck/cli@latest init --force # Or backup and regenerate cp app/api/upload/route.ts app/api/upload/route.ts.backup npx @pushduck/cli@latest init ``` ### Bucket Creation Failed ```bash # Test your credentials first npx @pushduck/cli@latest test # Skip automatic bucket creation npx @pushduck/cli@latest init --skip-bucket # Create bucket manually, then run: npx @pushduck/cli@latest test ``` ## Advanced Usage ### Custom Templates ```bash # Use custom file templates npx @pushduck/cli@latest init --template enterprise # Available templates: # - default: Basic setup with examples # - minimal: Just API routes, no examples # - enterprise: Full security and monitoring # - ecommerce: Product images and documents ``` ### Monorepo Support ```bash # For monorepos, specify the Next.js app directory cd apps/web npx @pushduck/cli@latest init # Or use the --cwd flag npx @pushduck/cli@latest init --cwd apps/web ``` ### CI/CD Integration ```bash # Non-interactive mode for CI/CD npx @pushduck/cli@latest init \ --provider aws \ --skip-examples \ --api-path /api/upload \ --no-interactive ``` *** **Complete CLI Reference**: This guide covers all CLI commands, options, and use cases. For a quick start, see our [Quick Start guide](/docs/quick-start). # API Reference (/docs/api) import { Card, Cards } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; ## Complete API Documentation Complete reference documentation for all pushduck APIs, from client-side hooks to server configuration and storage operations. **Type-Safe by Design**: All pushduck APIs are built with TypeScript-first design, providing excellent developer experience with full type inference and autocompletion. ## Client APIs * `useUpload` - Core upload hook with progress tracking * `useUploadRoute` - Route-specific uploads with validation **Perfect for**: React applications, reactive UIs * `createUploadClient` - Type-safe upload client * Property-based route access * Enhanced type inference **Perfect for**: Complex applications, better DX ## Server Configuration * Route definitions and validation * File type and size restrictions * Custom naming strategies **Essential for**: Setting up upload routes * Router configuration options * Middleware integration * Advanced routing patterns **Essential for**: Server setup and customization * Default upload options * Error handling configuration * Progress tracking settings **Essential for**: Client-side configuration * Dynamic path generation * Custom naming strategies * Folder organization **Essential for**: File organization ## Server APIs * Route definition and configuration * Built-in validation and middleware * Type-safe request/response handling **Core API**: The heart of pushduck * File listing and metadata * Delete operations * Presigned URL generation **Perfect for**: File management features ## Developer Tools * Project initialization * Component generation * Development utilities **Perfect for**: Quick setup and scaffolding * Error diagnosis and solutions * Performance optimization * Common gotchas and fixes **Essential for**: Problem solving ## Quick Reference ### Basic Server Setup ```typescript import { createS3Router, s3 } from 'pushduck/server'; const uploadRouter = createS3Router({ storage: { provider: 'aws-s3', region: 'us-east-1', bucket: 'my-bucket', credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }, routes: { imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB"), }, }); export const { GET, POST } = uploadRouter.handlers; ``` ### Basic Client Usage ```typescript import { useUpload } from 'pushduck/client'; function UploadComponent() { const { upload, uploading, progress } = useUpload({ endpoint: '/api/upload', route: 'imageUpload', }); const handleUpload = async (file: File) => { const result = await upload(file); console.log('Public URL:', result.url); // Permanent access console.log('Download URL:', result.presignedUrl); // Temporary access (1 hour) }; return (
handleUpload(e.target.files![0])} /> {uploading &&
Progress: {progress}%
}
); } ``` ## Architecture Overview **Getting Started**: New to pushduck? Start with the [Quick Start](/docs/quick-start) guide, then explore the specific APIs you need for your use case. ## API Categories | Category | Purpose | Best For | | ------------------- | ----------------------- | ---------------------------------------- | | **Client APIs** | Frontend file uploads | React components, user interactions | | **Server APIs** | Backend upload handling | Route definitions, validation | | **Storage APIs** | File management | Listing, deleting, URL generation | | **Configuration** | Setup and customization | Project configuration, advanced features | | **Developer Tools** | Development workflow | Setup, debugging, optimization | ## Next Steps 1. **New to pushduck?** → Start with [Quick Start](/docs/quick-start) 2. **Setting up uploads?** → Check [S3 Router](/docs/api/s3-router) 3. **Building UI?** → Explore [React Hooks](/docs/api/client) 4. **Managing files?** → Use [Storage API](/docs/api/storage) 5. **Need help?** → Visit [Troubleshooting](/docs/api/troubleshooting) # S3 Router (/docs/api/s3-router) ## S3 Router Configuration The S3 router provides a type-safe way to define upload endpoints with schema validation, middleware, and lifecycle hooks. ## Basic Router Setup ```typescript title="app/api/upload/route.ts" // app/api/upload/route.ts import { s3 } from '@/lib/upload' const s3Router = s3.createRouter({ imageUpload: s3 .image() .maxFileSize('5MB') .accept(['image/jpeg', 'image/png', 'image/webp']) .middleware(async ({ file, metadata }) => { // [!code highlight] // Add authentication and user context return { ...metadata, userId: 'user-123', uploadedAt: new Date().toISOString(), } }), documentUpload: s3 .file() .maxFileSize('10MB') .accept(['application/pdf', 'text/plain']) .paths({ prefix: 'documents', }), }) // Export the handler export const { GET, POST } = s3Router.handlers; // [!code highlight] ``` ## Schema Builders ### Image Schema ```typescript title="Image Schema Configuration" s3.image() .maxFileSize('5MB') // [!code highlight] .accept(['image/jpeg', 'image/png', 'image/webp', 'image/gif']) .dimensions({ minWidth: 100, maxWidth: 2000 }) .quality(0.8) // JPEG quality ``` ### File Schema ```typescript title="File Schema Configuration" s3.file() .maxFileSize('10MB') // [!code highlight] .accept(['application/pdf', 'text/plain', 'application/json']) ``` ### Object Schema (Multiple Files) ```typescript title="Object Schema Configuration" s3.object({ images: s3.image().maxFileSize('5MB').maxFiles(5), // [!code highlight] documents: s3.file().maxFileSize('10MB').maxFiles(2), thumbnail: s3.image().maxFileSize('1MB').maxFiles(1), }) ``` ## Route Configuration ### Middleware Add authentication, validation, and metadata: ```typescript title="Middleware Example" .middleware(async ({ file, metadata, req }) => { // [!code highlight] // Authentication const user = await authenticateUser(req) if (!user) { throw new Error('Authentication required') // [!code highlight] } // File validation if (file.size > 10 * 1024 * 1024) { throw new Error('File too large') } // Return enriched metadata return { ...metadata, // Client-provided metadata (e.g., albumId, tags) userId: user.id, // [!code highlight] userRole: user.role, uploadedAt: new Date().toISOString(), ipAddress: req.headers.get('x-forwarded-for'), } }) ``` **Client Metadata Support:** The `metadata` parameter contains data sent from the client via `uploadFiles(files, metadata)`. This allows passing UI context like album selections, tags, or form data. The middleware can then enrich this client metadata with server-side data like authenticated user information. **Example with Client Metadata:** ```typescript // Client component const { uploadFiles } = upload.imageUpload(); uploadFiles(files, { albumId: 'vacation-2025', tags: ['beach', 'sunset'], visibility: 'private' }); // Server middleware receives and validates .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); // Validate client-provided albumId if (metadata?.albumId) { const album = await db.albums.findFirst({ where: { id: metadata.albumId, userId: user.id // Ensure user owns the album } }); if (!album) throw new Error('Album not found or access denied'); } return { // Client metadata (validated) albumId: metadata?.albumId, tags: metadata?.tags || [], visibility: metadata?.visibility || 'private', // Server metadata (trusted) userId: user.id, // From auth, NOT from client role: user.role, // From auth, NOT from client uploadedAt: new Date().toISOString() }; }) ``` **Security Warning:** Client metadata is UNTRUSTED user input. Always validate and never trust client-provided identity claims (userId, role, permissions, etc.). Extract identity from authenticated sessions on the server. ### Path Configuration Control where files are stored: ```typescript .paths({ // Simple prefix prefix: 'user-uploads', // Custom path generation generateKey: (ctx) => { const { file, metadata, routeName } = ctx const userId = metadata.userId const timestamp = Date.now() return `${routeName}/${userId}/${timestamp}/${file.name}` }, // Simple suffix suffix: 'processed', }) ``` ### Lifecycle Hooks React to upload events: ```typescript .onUploadStart(async ({ file, metadata }) => { console.log(`Starting upload: ${file.name}`) // Log to analytics await analytics.track('upload_started', { userId: metadata.userId, filename: file.name, fileSize: file.size, }) }) .onUploadComplete(async ({ file, url, metadata }) => { console.log(`Upload complete: ${file.name} -> ${url}`) // Save to database await db.files.create({ filename: file.name, url, userId: metadata.userId, size: file.size, contentType: file.type, uploadedAt: new Date(), }) // Send notification await notificationService.send({ userId: metadata.userId, type: 'upload_complete', message: `${file.name} uploaded successfully`, }) }) .onUploadError(async ({ file, error, metadata }) => { console.error(`Upload failed: ${file.name}`, error) // Log error await errorLogger.log({ operation: 'file_upload', error: error.message, userId: metadata.userId, filename: file.name, }) }) ``` ### Presigned URL Expiry Control how long the generated presigned upload URL remains valid. Defaults to `3600` seconds (1 hour). ```typescript // Short-lived window — 5 minutes secureUpload: s3 .file() .maxFileSize('10MB') .expiresIn(300) // [!code highlight] // Extended window for large files — 2 hours largeFileUpload: s3 .file() .maxFileSize('500MB') .expiresIn(7200) // [!code highlight] ``` | Value | Duration | | ------- | ---------------- | | `300` | 5 minutes | | `3600` | 1 hour (default) | | `7200` | 2 hours | | `86400` | 24 hours | > **Note:** This controls the upload window — how long the client has to start the `PUT` to S3 after receiving the URL. Setting it too low may cause failures for large files or slow connections. Maximum is `604800` (7 days), enforced by AWS S3. ## Advanced Examples ### E-commerce Product Images ```typescript const productRouter = s3.createRouter({ productImages: s3 .image() .maxFileSize('5MB') .accept(['image/jpeg', 'image/png', 'image/webp']) .dimensions({ minWidth: 800, maxWidth: 2000 }) .middleware(async ({ metadata, req }) => { const user = await authenticateUser(req) const productId = metadata.productId // Verify user owns the product const product = await db.products.findFirst({ where: { id: productId, ownerId: user.id } }) if (!product) { throw new Error('Product not found or access denied') } return { ...metadata, userId: user.id, productId, productName: product.name, } }) .paths({ generateKey: (ctx) => { const { metadata } = ctx return `products/${metadata.productId}/images/${Date.now()}.jpg` } }) .onUploadComplete(async ({ url, metadata }) => { // Update product with new image await db.products.update({ where: { id: metadata.productId }, data: { images: { push: url } } }) }), productDocuments: s3 .file() .maxFileSize('10MB') .accept(['application/pdf']) .paths({ prefix: 'product-docs', }) .onUploadComplete(async ({ url, metadata }) => { await db.productDocuments.create({ productId: metadata.productId, documentUrl: url, type: 'specification', }) }), }) ``` ### User Profile System ```typescript const profileRouter = s3.createRouter({ avatar: s3 .image() .maxFileSize('2MB') .accept(['image/jpeg', 'image/png']) .dimensions({ minWidth: 100, maxWidth: 500 }) .middleware(async ({ req }) => { const user = await authenticateUser(req) return { userId: user.id, type: 'avatar' } }) .paths({ generateKey: (ctx) => { return `users/${ctx.metadata.userId}/avatar.jpg` } }) .onUploadComplete(async ({ url, metadata }) => { // Update user profile await db.users.update({ where: { id: metadata.userId }, data: { avatarUrl: url } }) // Invalidate cache await cache.del(`user:${metadata.userId}`) }), documents: s3 .object({ resume: s3.file().maxFileSize('5MB').accept(['application/pdf']).maxFiles(1), portfolio: s3.file().maxFileSize('10MB').maxFiles(3), }) .middleware(async ({ req }) => { const user = await authenticateUser(req) return { userId: user.id } }) .paths({ prefix: 'user-documents', }), }) ``` ## Client-Side Usage Once you have your router set up, use it from the client: ```typescript // components/FileUploader.tsx import { useUploadRoute } from 'pushduck' export function FileUploader() { const { upload, isUploading } = useUploadRoute('imageUpload') const handleUpload = async (files: FileList) => { try { const results = await upload(files, { // This metadata will be passed to middleware productId: 'product-123', category: 'main-images', }) console.log('Upload complete:', results) } catch (error) { console.error('Upload failed:', error) } } return (
e.target.files && handleUpload(e.target.files)} disabled={isUploading} /> {isUploading &&

Uploading...

}
) } ``` ## Type Safety The router provides full TypeScript support: ```typescript // Types are automatically inferred type RouterType = typeof s3Router // Get route names type RouteNames = keyof RouterType // 'imageUpload' | 'documentUpload' // Get route input types type ImageUploadInput = InferRouteInput // Get route metadata types type ImageUploadMetadata = InferRouteMetadata ``` # Troubleshooting (/docs/api/troubleshooting) import { Callout } from "fumadocs-ui/components/callout"; import { Tabs, Tab } from "fumadocs-ui/components/tabs"; ## Common Issues and Solutions Common issues and solutions when using pushduck. ## Development Issues ### Next.js Turbo Mode Compatibility **Known Issue:** pushduck has compatibility issues with Next.js Turbo mode (`--turbo` flag). **Problem:** Uploads fail or behave unexpectedly when using `next dev --turbo`. **Solution:** Remove the `--turbo` flag from your development script: ```json { "scripts": { // ❌ This may cause issues "dev": "next dev --turbo", // ✅ Use this instead "dev": "next dev" } } ``` ```bash # ❌ This may cause issues npm run dev --turbo # ✅ Use this instead npm run dev ``` **Why this happens:** Turbo mode's aggressive caching and bundling can interfere with the upload process, particularly with presigned URL generation and file streaming. ## Upload Failures ### CORS Errors **Problem:** Browser console shows CORS errors when uploading files. **Symptoms:** ``` Access to XMLHttpRequest at 'https://bucket.s3.amazonaws.com/...' from origin 'http://localhost:3000' has been blocked by CORS policy ``` **Solution:** Configure CORS on your storage bucket. **Comprehensive CORS Guide:** For detailed CORS configuration, testing, and troubleshooting across all providers, see the [CORS & ACL Configuration Guide](/docs/guides/security/cors-and-acl). **Quick fixes:** * See the [provider setup guides](/docs/providers) for basic CORS configuration * Ensure your domain is included in `AllowedOrigins` * Verify all required HTTP methods are allowed (`PUT`, `POST`, `GET`) * Check that required headers are included in `AllowedHeaders` ### Environment Variables Not Found **Problem:** Errors about missing environment variables. **Symptoms:** ``` Error: Environment variable CLOUDFLARE_R2_ACCESS_KEY_ID is not defined ``` **Solution:** Ensure your environment variables are properly set: 1. **Check your `.env.local` file exists** in your project root 2. **Verify variable names** match exactly (case-sensitive) 3. **Restart your development server** after adding new variables ```bash # .env.local CLOUDFLARE_R2_ACCESS_KEY_ID=your_access_key CLOUDFLARE_R2_SECRET_ACCESS_KEY=your_secret_key CLOUDFLARE_R2_ACCOUNT_ID=your_account_id R2_BUCKET=your-bucket-name ``` ### File Size Limits **Problem:** Large files fail to upload. **Solution:** Check and adjust size limits: ```typescript // app/api/upload/route.ts const uploadRouter = s3.createRouter({ imageUpload: s3 .image() .maxFileSize("10MB") // Increase as needed .accept(["image/jpeg", "image/png", "image/webp"]), }); ``` ## Type Errors ### TypeScript Inference Issues **Problem:** TypeScript errors with upload client. **Solution:** Ensure proper type exports: ```typescript // app/api/upload/route.ts export const { GET, POST } = uploadRouter.handlers; export type AppRouter = typeof uploadRouter; // ✅ Export the type // lib/upload-client.ts import type { AppRouter } from "@/app/api/upload/route"; export const upload = createUploadClient({ // ✅ Use the type endpoint: "/api/upload", }); ``` ## Performance Issues ### Slow Upload Speeds **Problem:** Uploads are slower than expected. **Solutions:** 1. **Choose the right provider region** close to your users 2. **Check your internet connection** and server resources 3. **Consider your provider's performance characteristics** ### Memory Issues with Large Files **Problem:** Browser crashes or high memory usage with large files. **Solution:** File streaming is handled automatically by pushduck: ```typescript // File streaming is handled automatically // No additional configuration needed const { uploadFiles } = upload.fileUpload(); await uploadFiles(largeFiles); // ✅ Streams automatically ``` ## Getting Help If you're still experiencing issues: 1. **Check the documentation** for your specific provider 2. **For CORS/ACL issues** see the [CORS & ACL Configuration Guide](/docs/guides/security/cors-and-acl) 3. **Enable debug logging** by setting `NODE_ENV=development` 4. **Check browser console** for detailed error messages 5. **Verify your provider configuration** is correct **Need more help?** Create an issue on [GitHub](https://github.com/abhay-ramesh/pushduck/issues) with detailed information about your setup and the error you're experiencing. # Client-Side Approaches (/docs/guides/client-approaches) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Client-Side Approaches Pushduck provides **two ways** to integrate file uploads in your React components. Both approaches now provide **identical functionality** including per-route callbacks, progress tracking, and error handling. **Recommendation**: Use the **Enhanced Structured Client** approach for the best developer experience. It now provides the same flexibility as hooks while maintaining superior type safety and centralized configuration. ## Quick Comparison ```typescript const upload = createUploadClient({ endpoint: '/api/upload' }) // Simple usage const { uploadFiles, files } = upload.imageUpload() // With per-route callbacks (NEW!) const { uploadFiles, files } = upload.imageUpload({ onStart: (files) => setUploadStarted(true), onSuccess: (results) => handleSuccess(results), onError: (error) => handleError(error), onProgress: (progress) => setProgress(progress) }) ``` **Best for**: Most projects - provides superior DX, type safety, and full flexibility ```typescript const { uploadFiles, files } = useUploadRoute('imageUpload', { onStart: (files) => setUploadStarted(true), onSuccess: (results) => handleSuccess(results), onError: (error) => handleError(error), onProgress: (progress) => setProgress(progress) }) ``` **Best for**: Teams that strongly prefer React hooks, legacy code migration ## Feature Parity Both approaches now support **identical functionality**: | Feature | Enhanced Structured Client | Hook-Based | | --------------------- | -------------------------------- | ---------------------------- | | ✅ Type Safety | **Superior** - Property-based | Good - Generic types | | ✅ Per-route Callbacks | **✅ Full support** | ✅ Full support | | ✅ Progress Tracking | **✅ Full support** | ✅ Full support | | ✅ Error Handling | **✅ Full support** | ✅ Full support | | ✅ Multiple Endpoints | **✅ Per-route endpoints** | ✅ Per-route endpoints | | ✅ Upload Control | **✅ Enable/disable uploads** | ✅ Enable/disable uploads | | ✅ Auto-upload | **✅ Per-route control** | ✅ Per-route control | | ✅ Overall Progress | **✅ progress, uploadSpeed, eta** | ✅ progress, uploadSpeed, eta | ## API Comparison: Identical Capabilities Both approaches now return **exactly the same** properties and accept **exactly the same** configuration options: ```typescript // Hook-Based Approach const { uploadFiles, // (files: File[]) => Promise files, // S3UploadedFile[] isUploading, // boolean errors, // string[] reset, // () => void progress, // number (0-100) - overall progress uploadSpeed, // number (bytes/sec) - overall speed eta // number (seconds) - overall ETA } = useUploadRoute('imageUpload', { onStart: (files) => setUploadStarted(true), onSuccess: (results) => handleSuccess(results), onError: (error) => handleError(error), onProgress: (progress) => setProgress(progress), endpoint: '/api/custom-upload', }); // Enhanced Structured Client - IDENTICAL capabilities const { uploadFiles, // (files: File[]) => Promise files, // S3UploadedFile[] isUploading, // boolean errors, // string[] reset, // () => void progress, // number (0-100) - overall progress uploadSpeed, // number (bytes/sec) - overall speed eta // number (seconds) - overall ETA } = upload.imageUpload({ onStart: (files) => setUploadStarted(true), onSuccess: (results) => handleSuccess(results), onError: (error) => handleError(error), onProgress: (progress) => setProgress(progress), endpoint: '/api/custom-upload', }); ``` ## Complete Options Parity Both approaches support **identical configuration options**: ```typescript interface CommonUploadOptions { onStart?: (files: S3FileMetadata[]) => void; onSuccess?: (results: UploadResult[]) => void; onError?: (error: Error) => void; onProgress?: (progress: number) => void; endpoint?: string; // Custom endpoint per route } // Hook-based: useUploadRoute(routeName, options) // Structured: upload.routeName(options) // Both accept the same CommonUploadOptions interface ``` ## Return Value Parity Both approaches return **identical properties**: ```typescript interface CommonUploadReturn { uploadFiles: (files: File[]) => Promise; files: S3UploadedFile[]; isUploading: boolean; errors: string[]; reset: () => void; // Overall progress tracking (NEW in both!) progress?: number; // 0-100 percentage across all files uploadSpeed?: number; // bytes per second across all files eta?: number; // seconds remaining for all files } ``` ## Enhanced Structured Client Examples ### Basic Usage (Unchanged) ```typescript import { createUploadClient } from 'pushduck/client' import type { AppRouter } from '@/lib/upload' const upload = createUploadClient({ endpoint: '/api/upload' }) export function SimpleUpload() { const { uploadFiles, files, isUploading } = upload.imageUpload() return ( uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> ) } ``` ### With Per-Route Configuration (NEW!) ```typescript export function AdvancedUpload() { const [progress, setProgress] = useState(0) const { uploadFiles, files, isUploading, errors, reset } = upload.imageUpload({ onStart: (files) => { console.log('Upload starting!', files) setUploadStarted(true) }, onSuccess: (results) => { console.log('✅ Upload successful!', results) results.forEach(file => { console.log('Public URL:', file.url); // Permanent access console.log('Download URL:', file.presignedUrl); // Temporary access (1 hour) }); showNotification('Images uploaded successfully!') setUploadStarted(false) }, onError: (error) => { console.error('❌ Upload failed:', error) showErrorNotification(error.message) setUploadStarted(false) }, onProgress: (progress) => { console.log(`📊 Progress: ${progress}%`) setProgress(progress) } }) return (
uploadFiles(Array.from(e.target.files || []))} /> {progress > 0 && }
) } ``` ### Multiple Routes with Different Configurations ```typescript export function MultiUploadComponent() { // Images with progress tracking const images = upload.imageUpload({ onStart: (files) => setUploadingImages(true), onProgress: (progress) => setImageProgress(progress) }) // Documents with different endpoint and success handler const documents = upload.documentUpload({ endpoint: '/api/secure-upload', onStart: (files) => setUploadingDocuments(true), onSuccess: (results) => { // Use presigned URLs for private document downloads updateDocumentLibrary(results.map(file => ({ id: file.id, name: file.name, downloadUrl: file.presignedUrl, // Secure, time-limited access permanentUrl: file.url, // For internal operations key: file.key }))); } }) // Videos with conditional logic in component const videos = upload.videoUpload({ onStart: (files) => setUploadingVideos(true) }) return (
) } ``` ### Global Configuration with Per-Route Overrides ```typescript const upload = createUploadClient({ endpoint: '/api/upload', // Global defaults (optional) defaultOptions: { onStart: (files) => console.log(`Starting upload of ${files.length} files`), onProgress: (progress) => console.log(`Global progress: ${progress}%`), onError: (error) => logError(error) } }) // This route inherits global defaults const basic = upload.imageUpload() // This route overrides specific options const custom = upload.documentUpload({ endpoint: '/api/secure-upload', // Override endpoint onSuccess: (results) => handleSecureUpload(results) // Add success handler // Still inherits global onProgress and onError }) ``` ## Hook-Based Approach (Unchanged) ```typescript import { useUploadRoute } from 'pushduck/client' export function HookBasedUpload() { const { uploadFiles, files, isUploading, error } = useUploadRoute('imageUpload', { onStart: (files) => console.log('Starting upload:', files), onSuccess: (results) => console.log('Success:', results), onError: (error) => console.error('Error:', error), onProgress: (progress) => console.log('Progress:', progress) }) return ( uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> ) } ``` ## Migration Guide ### From Hook-Based to Enhanced Structured Client ```typescript // Before: Hook-based const { uploadFiles, files } = useUploadRoute('imageUpload', { onStart: handleStart, onSuccess: handleSuccess, onError: handleError }) // After: Enhanced structured client const upload = createUploadClient({ endpoint: '/api/upload' }) const { uploadFiles, files } = upload.imageUpload({ onStart: handleStart, onSuccess: handleSuccess, onError: handleError }) ``` ### Benefits of Migration 1. **Better Type Safety**: Route names are validated at compile time 2. **Enhanced IntelliSense**: Auto-completion for all available routes 3. **Centralized Configuration**: Single place to configure endpoints and defaults 4. **Refactoring Support**: Rename routes safely across your codebase 5. **No Performance Impact**: Same underlying implementation ## When to Use Each Approach ### Use Enhanced Structured Client When: * ✅ **Starting a new project** - best overall developer experience * ✅ **Want superior type safety** - compile-time route validation * ✅ **Need centralized configuration** - single place for settings * ✅ **Value refactoring support** - safe route renames ### Use Hook-Based When: * ✅ **Migrating existing code** - minimal changes required * ✅ **Dynamic route names** - routes determined at runtime * ✅ **Team strongly prefers hooks** - familiar React patterns * ✅ **Legacy compatibility** - maintaining older codebases ## Performance Considerations Both approaches have **identical performance** characteristics: * Same underlying `useUploadRoute` implementation * Same network requests and upload logic * Same React hooks rules and lifecycle The enhanced structured client adds zero runtime overhead while providing compile-time benefits. *** **Full Feature Parity**: Both approaches now support the same functionality. The choice comes down to developer experience preferences rather than feature limitations. ## Detailed Comparison ### Type Safety & Developer Experience ```typescript // ✅ Complete type inference from server router const upload = createUploadClient({ endpoint: '/api/upload' }) // ✅ Property-based access - no string literals const { uploadFiles, files } = upload.imageUpload() // ✅ IntelliSense shows all available endpoints upload. // <- Shows: imageUpload, documentUpload, videoUpload... // ✅ Compile-time validation upload.nonExistentRoute() // ❌ TypeScript error // ✅ Refactoring safety // Rename routes in router → TypeScript shows all usage locations ``` **Benefits:** * 🎯 **Full type inference** from server to client * 🔍 **IntelliSense support** - discover endpoints through IDE * 🛡️ **Refactoring safety** - rename with confidence * 🚫 **No string literals** - eliminates typos * ⚡ **Better DX** - property-based access feels natural ```typescript // ✅ With type parameter - recommended for better type safety const { uploadFiles, files } = useUploadRoute('imageUpload') // ✅ Without type parameter - also works const { uploadFiles, files } = useUploadRoute('imageUpload') // Type parameter provides compile-time validation const typed = useUploadRoute('imageUpload') // Route validated const untyped = useUploadRoute('imageUpload') // Any string accepted ``` **Characteristics:** * 🪝 **React hook pattern** - familiar to React developers * 🔤 **Flexible usage** - works with or without type parameter * 🧩 **Component-level state** - each hook manages its own state * 🎯 **Type safety** - enhanced when using `` * 🔍 **IDE support** - best with type parameter ### Code Examples **Structured Client:** ```typescript import { upload } from '@/lib/upload-client' export function ImageUploader() { const { uploadFiles, files, isUploading, error } = upload.imageUpload() return (
uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> {/* Upload UI */}
) } ``` **Hook-Based:** ```typescript import { useUploadRoute } from 'pushduck/client' export function ImageUploader() { const { uploadFiles, files, isUploading, error } = useUploadRoute('imageUpload') return (
uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> {/* Same upload UI */}
) } ```
**Structured Client:** ```typescript export function FileManager() { const images = upload.imageUpload() const documents = upload.documentUpload() const videos = upload.videoUpload() return (
) } ``` **Hook-Based:** ```typescript export function FileManager() { const images = useUploadRoute('imageUpload') const documents = useUploadRoute('documentUpload') const videos = useUploadRoute('videoUpload') return (
) } ```
**Structured Client:** ```typescript // lib/upload-client.ts export const upload = createUploadClient({ endpoint: '/api/upload', headers: { Authorization: `Bearer ${getAuthToken()}` } }) // components/secure-uploader.tsx export function SecureUploader() { const { uploadFiles } = upload.secureUpload() // Authentication handled globally } ``` **Hook-Based:** ```typescript export function SecureUploader() { const { uploadFiles } = useUploadRoute('secureUpload', { headers: { Authorization: `Bearer ${getAuthToken()}` } }) // Authentication per hook usage } ```
## Conclusion **Our Recommendation**: Use the **Enhanced Structured Client** approach (`createUploadClient`) for most projects. It provides superior developer experience, better refactoring safety, and enhanced type inference. **Both approaches are supported**: The hook-based approach (`useUploadRoute`) is fully supported and valid for teams that prefer traditional React patterns. **Quick Decision Guide:** * **Most projects** → Use `createUploadClient` (recommended) * **Strongly prefer React hooks** → Use `useUploadRoute` * **Want best DX and type safety** → Use `createUploadClient` * **Need component-level control** → Use `useUploadRoute` ### Next Steps * **New Project**: Start with [createUploadClient](/docs/api/client/create-upload-client) * **Existing Hook Code**: Consider [migrating gradually](/docs/guides/migrate-to-enhanced-client) * **Need Help**: Join our [Discord community](https://pushduck.dev/discord) for guidance # Client-Side Metadata (/docs/guides/client-metadata) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Client-Side Metadata Client-side metadata allows you to pass contextual information from your UI directly to the server during file uploads. This enables dynamic file organization, categorization, and processing based on user selections and application state. **New Feature:** As of v0.1.23, you can now pass metadata from the client when calling `uploadFiles()`. This metadata flows through to your middleware, lifecycle hooks, and path generation functions. ## Why Use Client Metadata? Client metadata bridges the gap between UI context and server-side processing: * **UI State** - Pass album selections, categories, or form data * **Multi-tenant Context** - Send workspace, project, or organization IDs * **User Preferences** - Include tags, visibility settings, or custom fields * **Dynamic Organization** - Organize files based on client context * **Flexible Workflows** - Adapt to different use cases without API changes ## Basic Usage **Client: Pass metadata with uploadFiles** ```typescript import { upload } from '@/lib/upload-client'; export function ImageUploader() { const { uploadFiles } = upload.imageUpload(); const handleUpload = (files: File[]) => { // Pass metadata as second parameter uploadFiles(files, { albumId: 'vacation-2025', tags: ['beach', 'sunset'], visibility: 'private' }); }; return handleUpload(Array.from(e.target.files || []))} />; } ``` **Server: Receive in middleware** ```typescript // app/api/upload/route.ts const s3Router = s3.createRouter({ imageUpload: s3.image() .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); return { ...metadata, // Client data: { albumId, tags, visibility } userId: user.id, // Server data from auth }; }) }); ``` **Use in hooks and path generation** ```typescript .paths({ generateKey: (ctx) => { // Metadata available in path generation return `users/${ctx.metadata.userId}/albums/${ctx.metadata.albumId}/${ctx.file.name}`; } }) .onUploadComplete(async ({ metadata, url }) => { // Metadata available in lifecycle hooks await db.images.create({ url, albumId: metadata.albumId, tags: metadata.tags, userId: metadata.userId }); }) ``` ## Real-World Examples ### Multi-Tenant SaaS Application ```typescript // Client component export function WorkspaceFileUpload({ workspace, project }: Props) { const { uploadFiles } = upload.documentUpload(); const handleUpload = (files: File[]) => { uploadFiles(files, { workspaceId: workspace.id, projectId: project.id, teamId: workspace.team.id, folder: selectedFolder.path, permissions: { canEdit: currentUser.role === 'admin', canDelete: currentUser.role === 'admin', canShare: true } }); }; return ; } ``` ```typescript // Server middleware - validates tenant isolation .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); // Verify user belongs to workspace const membership = await db.workspaceMemberships.findFirst({ where: { workspaceId: metadata?.workspaceId, userId: user.id } }); if (!membership) { throw new Error('Access denied to workspace'); } // Verify user can access project const project = await db.projects.findFirst({ where: { id: metadata?.projectId, workspaceId: metadata?.workspaceId } }); if (!project) { throw new Error('Project not found'); } return { // Validated client metadata workspaceId: metadata.workspaceId, projectId: metadata.projectId, folder: metadata.folder || '/', // Server metadata userId: user.id, userRole: membership.role, uploadedAt: new Date().toISOString() }; }) .paths({ generateKey: (ctx) => { const { metadata, file } = ctx; return `workspaces/${metadata.workspaceId}/projects/${metadata.projectId}${metadata.folder}/${file.name}`; } }) ``` ### E-Commerce Product Images ```typescript // Client: Product image upload with variants export function ProductImageManager({ product }: { product: Product }) { const [imageType, setImageType] = useState<'main' | 'gallery' | 'thumbnail'>('gallery'); const [selectedVariant, setSelectedVariant] = useState(null); const { uploadFiles, files } = upload.productImages(); const handleUpload = (files: File[]) => { uploadFiles(files, { productId: product.id, variantId: selectedVariant, imageType: imageType, sortOrder: product.images.length + 1, altText: `${product.name} - ${imageType} image` }); }; return (
{product.variants.length > 0 && ( )} e.target.files && handleUpload(Array.from(e.target.files))} />
); } ``` ```typescript // Server: Organize by product and variant .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); // Verify user owns product const product = await db.products.findFirst({ where: { id: metadata?.productId, ownerId: user.id } }); if (!product) { throw new Error('Product not found or access denied'); } return { productId: metadata.productId, variantId: metadata.variantId, imageType: metadata.imageType, sortOrder: metadata.sortOrder, userId: user.id, merchantId: product.merchantId }; }) .paths({ generateKey: (ctx) => { const { metadata, file } = ctx; const variantPath = metadata.variantId ? `/variants/${metadata.variantId}` : ''; return `products/${metadata.productId}${variantPath}/${metadata.imageType}/${metadata.sortOrder}-${file.name}`; } }) .onUploadComplete(async ({ metadata, url }) => { await db.productImages.create({ productId: metadata.productId, variantId: metadata.variantId, type: metadata.imageType, url: url, sortOrder: metadata.sortOrder, altText: metadata.altText }); }) ``` ### Content Management System ```typescript // Client: Content upload with categorization export function CMSMediaUpload() { const [contentType, setContentType] = useState('article'); const [category, setCategory] = useState('technology'); const [tags, setTags] = useState([]); const [publishDate, setPublishDate] = useState(''); const { uploadFiles } = upload.mediaUpload(); const handleUpload = (files: File[]) => { uploadFiles(files, { contentType: contentType, category: category, tags: tags, publishDate: publishDate || new Date().toISOString(), featured: false, author: currentUser.username }); }; return (
setPublishDate(e.target.value)} /> e.target.files && handleUpload(Array.from(e.target.files))} />
); } ``` ```typescript // Server: CMS organization .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); // Verify user has content creation permissions if (!user.permissions.includes('create:content')) { throw new Error('Insufficient permissions'); } return { contentType: metadata?.contentType || 'article', category: metadata?.category, tags: metadata?.tags || [], publishDate: metadata?.publishDate, authorId: user.id, authorName: user.name, status: 'draft' }; }) .paths({ generateKey: (ctx) => { const { metadata, file } = ctx; const date = new Date(metadata.publishDate); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); return `content/${metadata.contentType}/${year}/${month}/${metadata.category}/${file.name}`; } }) ``` ## Security Best Practices **⚠️ CRITICAL: Client metadata is UNTRUSTED user input.** Never trust identity claims, permissions, or security-related data from the client. Always validate and extract identity from authenticated sessions on the server. ### DON'T: Trust Client Identity ```typescript // ❌ BAD: Trusting client-provided userId uploadFiles(files, { userId: currentUser.id, // Client can fake this isAdmin: true, // Client can lie about this role: 'admin' // Never trust this from client }); .middleware(async ({ metadata }) => { return { userId: metadata.userId, // ❌ DANGEROUS! role: metadata.role // ❌ SECURITY RISK! }; }) ``` ### DO: Validate and Override ```typescript // ✅ GOOD: Server determines identity uploadFiles(files, { albumId: selectedAlbum.id, // ✅ OK - contextual data tags: selectedTags, // ✅ OK - user input (validate) visibility: 'private' // ✅ OK - user preference }); .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); // Server auth // Validate client data if (metadata?.albumId) { const album = await db.albums.findFirst({ where: { id: metadata.albumId, userId: user.id // Verify ownership } }); if (!album) throw new Error('Invalid album'); } return { // Client metadata (validated) albumId: metadata?.albumId, tags: sanitizeTags(metadata?.tags || []), // Server metadata (trusted) userId: user.id, // ✅ From auth role: user.role, // ✅ From auth uploadedAt: new Date().toISOString() }; }) ``` ## Validation Strategies ### Type Validation ```typescript .middleware(async ({ metadata }) => { // Validate data types if (metadata?.albumId && typeof metadata.albumId !== 'string') { throw new Error('Invalid albumId type'); } if (metadata?.tags && !Array.isArray(metadata.tags)) { throw new Error('Tags must be an array'); } if (metadata?.sortOrder && typeof metadata.sortOrder !== 'number') { throw new Error('Invalid sortOrder type'); } return { ...metadata }; }) ``` ### Value Validation ```typescript .middleware(async ({ metadata }) => { // Validate UUIDs const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (metadata?.albumId && !uuidRegex.test(metadata.albumId)) { throw new Error('Invalid album ID format'); } // Validate enums const validVisibilities = ['public', 'private', 'unlisted']; if (metadata?.visibility && !validVisibilities.includes(metadata.visibility)) { throw new Error('Invalid visibility setting'); } // Sanitize strings const sanitizedTags = (metadata?.tags || []).map((tag: string) => tag.trim().toLowerCase().replace(/[^a-z0-9-]/g, '') ); return { ...metadata, tags: sanitizedTags }; }) ``` ### Database Validation ```typescript .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); // Verify referenced entities exist and user has access if (metadata?.projectId) { const project = await db.projects.findFirst({ where: { id: metadata.projectId, members: { some: { userId: user.id } } } }); if (!project) { throw new Error('Project not found or access denied'); } } if (metadata?.folderId) { const folder = await db.folders.findFirst({ where: { id: metadata.folderId, projectId: metadata.projectId, deleted: false } }); if (!folder) { throw new Error('Folder not found'); } } return { ...metadata, userId: user.id }; }) ``` ## TypeScript Support Define metadata interfaces for better type safety: ```typescript // Define your metadata interface interface UploadMetadata { albumId: string; tags: string[]; visibility: 'public' | 'private' | 'unlisted'; featured?: boolean; } // Client usage with type safety const handleUpload = (files: File[]) => { const metadata: UploadMetadata = { albumId: selectedAlbum.id, tags: selectedTags, visibility: visibilityOption, featured: isFeatured }; uploadFiles(files, metadata); }; // Server middleware with typed metadata .middleware(async ({ req, metadata }: { req: NextRequest; metadata?: UploadMetadata }) => { const user = await authenticateUser(req); return { ...metadata, userId: user.id }; }) ``` ## Common Use Cases ### Album/Gallery Organization ```typescript // Pass album selection from UI uploadFiles(files, { albumId: selectedAlbum.id, albumName: selectedAlbum.name, tags: selectedTags, visibility: albumSettings.defaultVisibility }); ``` ### Document Management ```typescript // Pass folder structure and metadata uploadFiles(files, { folderId: currentFolder.id, folderPath: currentFolder.fullPath, category: documentCategory, confidential: isConfidential, expiresAt: expirationDate }); ``` ### User Profile Assets ```typescript // Pass asset type and purpose uploadFiles(files, { assetType: 'profile-picture', purpose: 'avatar', aspectRatio: '1:1', previousAssetId: currentAvatar?.id // For cleanup }); ``` ### Form Submissions ```typescript // Pass form context with uploads uploadFiles(files, { formId: formSubmission.id, formType: 'contact', attachmentType: 'supporting-document', relatedTo: formData.ticketId }); ``` ## Advanced Patterns ### Conditional Metadata ```typescript const handleUpload = (files: File[]) => { const metadata: any = { uploadSource: 'web-app', timestamp: Date.now() }; // Conditionally add metadata if (selectedAlbum) { metadata.albumId = selectedAlbum.id; } if (tags.length > 0) { metadata.tags = tags; } if (isAdminUser) { metadata.priority = 'high'; metadata.skipModeration = true; } uploadFiles(files, metadata); }; ``` ### Metadata from Form State ```typescript import { useForm } from 'react-hook-form'; export function FormWithUploads() { const { register, handleSubmit } = useForm(); const { uploadFiles } = upload.attachments(); const onSubmit = async (formData: any) => { // Upload files with form context await uploadFiles(selectedFiles, { formId: formData.id, category: formData.category, priority: formData.priority, department: formData.department, requestedBy: formData.requesterEmail }); // Then submit form await submitForm(formData); }; return
...
; } ``` ### Dynamic Path Generation ```typescript // Client uploadFiles(files, { organizationId: org.id, departmentId: dept.id, projectCode: project.code, fileClass: 'confidential' }); // Server - organize by metadata .paths({ generateKey: (ctx) => { const { metadata, file } = ctx; const date = new Date(); const year = date.getFullYear(); const quarter = `Q${Math.ceil((date.getMonth() + 1) / 3)}`; return [ 'organizations', metadata.organizationId, 'departments', metadata.departmentId, year.toString(), quarter, metadata.fileClass, metadata.projectCode, file.name ].join('/'); } }) ``` ## Metadata Size Considerations **Size Limits:** While there's no hard limit on metadata size, keep it reasonable (\< 10KB). Large metadata objects increase request size and processing time. ### Good Metadata ```typescript // Compact and purposeful { albumId: 'abc123', tags: ['vacation', 'beach'], visibility: 'private', featured: false } ``` ### Avoid Large Metadata ```typescript // Too large - send via separate API call { albumId: 'abc123', fullImageData: base64EncodedImage, // ❌ Don't embed files entireUserProfile: { ... }, // ❌ Too much data allPreviousUploads: [ ... ], // ❌ Unnecessary complexNestedStructure: { ... } // ⚠️ Keep it simple } ``` ## Error Handling Handle metadata-related errors gracefully: ```typescript const { uploadFiles } = upload.imageUpload({ onError: (error) => { if (error.message.includes('album')) { toast.error('Selected album is invalid or you don\'t have access'); resetAlbumSelection(); } else if (error.message.includes('metadata')) { toast.error('Invalid upload settings. Please try again.'); } else { toast.error('Upload failed: ' + error.message); } } }); ``` ## Testing with Metadata ```typescript import { render, fireEvent } from '@testing-library/react'; test('uploads files with correct metadata', async () => { const mockUploadFiles = vi.fn(); const { getByLabelText, getByRole } = render( ); // Select album const albumSelect = getByLabelText('Album'); fireEvent.change(albumSelect, { target: { value: 'vacation-2025' } }); // Add tags const tagsInput = getByLabelText('Tags'); fireEvent.change(tagsInput, { target: { value: 'beach,sunset' } }); // Upload files const fileInput = getByRole('file-input'); const files = [new File(['content'], 'photo.jpg', { type: 'image/jpeg' })]; fireEvent.change(fileInput, { target: { files } }); // Verify metadata was passed correctly expect(mockUploadFiles).toHaveBeenCalledWith( expect.arrayContaining(files), expect.objectContaining({ albumId: 'vacation-2025', tags: ['beach', 'sunset'] }) ); }); ``` ## Best Practices ### DO * Pass UI state and user selections * Validate metadata in middleware * Use metadata for dynamic path generation * Keep metadata size reasonable (\< 10KB) * Define TypeScript interfaces for metadata * Sanitize user input (tags, descriptions) * Verify access to referenced entities (albums, projects) ### DON'T * Trust client-provided identity (userId, role, permissions) * Send sensitive data (passwords, tokens, secrets) * Embed large objects (base64 files, entire datasets) * Skip validation in middleware * Use metadata for authentication * Trust metadata without verification ## Migration Guide If you're upgrading from a version without metadata support: ```typescript // Before: No metadata support const { uploadFiles } = upload.imageUpload(); uploadFiles(files); // After: Add metadata (backward compatible) const { uploadFiles } = upload.imageUpload(); // Still works without metadata uploadFiles(files); // Or add metadata uploadFiles(files, { albumId: album.id }); ``` The feature is **100% backward compatible** - existing code continues to work without changes. *** # Image Uploads (/docs/guides/image-uploads) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { TypeTable } from "fumadocs-ui/components/type-table"; import { Files, Folder, File } from "fumadocs-ui/components/files"; ## Image Upload Guide Handle image uploads with built-in optimization, validation, and processing features for the best user experience. Images are the most common upload type. This guide covers everything from basic setup to advanced optimization techniques for production apps. ## Basic Image Upload Setup ### Server Configuration ```typescript // app/api/upload/route.ts import { s3 } from "@/lib/upload"; const s3Router = s3.createRouter({ // Basic image upload profilePicture: s3.image() .maxFileSize('5MB') .maxFiles(1) .accept(['image/jpeg', 'image/png', 'image/webp']), // Multiple images with optimization galleryImages: s3.image() .maxFileSize('10MB') .maxFiles(10) .accept(['image/jpeg', 'image/png', 'image/webp', 'image/gif']), }); export type AppS3Router = typeof s3Router; export const { GET, POST } = s3Router.handlers; ``` ### Client Implementation ```typescript // components/image-uploader.tsx import { upload } from "@/lib/upload-client"; export function ImageUploader() { const { uploadFiles, files, isUploading } = upload.galleryImages; const handleImageSelect = (e: React.ChangeEvent) => { const selectedFiles = Array.from(e.target.files || []); uploadFiles(selectedFiles); }; return (
{files.map((file) => (
{file.status === "success" && ( {file.name} )} {file.status === "uploading" && (
{file.progress}%
)}
))}
); } ``` ## Client-Side Metadata for Images Pass contextual information from your UI to organize and categorize images: **New Feature:** You can now pass metadata directly from the client when uploading images. This allows you to send album IDs, tags, categories, or any contextual data from your UI to the server. ```typescript // Client component import { upload } from '@/lib/upload-client'; import { useState } from 'react'; export function GalleryUpload() { const [selectedAlbum, setSelectedAlbum] = useState('vacation-2025'); const [tags, setTags] = useState([]); const [isFeatured, setIsFeatured] = useState(false); const { uploadFiles, files } = upload.galleryImages({ onSuccess: (results) => { console.log(`Uploaded to album: ${selectedAlbum}`); } }); const handleUpload = (selectedFiles: File[]) => { // Pass UI state as metadata uploadFiles(selectedFiles, { albumId: selectedAlbum, tags: tags, featured: isFeatured, uploadSource: 'gallery-manager', category: 'user-content' }); }; return (
setIsFeatured(e.target.checked)} /> e.target.files && handleUpload(Array.from(e.target.files))} />
); } ``` **Server-side validation:** ```typescript // Server router .middleware(async ({ req, metadata }) => { const user = await authenticateUser(req); // Validate client-provided album access if (metadata?.albumId) { const hasAccess = await db.albums.canUserAccess(metadata.albumId, user.id); if (!hasAccess) throw new Error('Access denied to album'); } return { // Client metadata (validated) albumId: metadata?.albumId, tags: metadata?.tags || [], featured: metadata?.featured || false, // Server metadata (trusted) userId: user.id, uploadedAt: new Date().toISOString() }; }) .paths({ generateKey: (ctx) => { const { metadata, file } = ctx; // Use metadata in path generation return `albums/${metadata.albumId}/${metadata.featured ? 'featured/' : ''}${file.name}`; } }) ``` **Security:** Always validate client metadata in middleware. Never trust client-provided user IDs or permissions. ## Image Validation & Processing ### Format Validation ```typescript const s3Router = s3.createRouter({ productImages: s3.image() .maxFileSize('8MB') .maxFiles(5) .accept(['image/jpeg', 'image/png', 'image/webp']) .dimensions({ minWidth: 800, maxWidth: 4000, minHeight: 600, maxHeight: 3000, }) .aspectRatio(16 / 9, { tolerance: 0.1 }) .middleware(async ({ req, file, metadata }) => { // Custom validation const imageMetadata = await getImageMetadata(file); if ( imageMetadata.hasTransparency && !["png", "webp"].includes(imageMetadata.format) ) { throw new Error("Transparent images must be PNG or WebP format"); } if (imageMetadata.colorProfile !== "sRGB") { console.warn( `Image ${file.name} uses ${imageMetadata.colorProfile} color profile` ); } return { ...metadata, userId: await getUserId(req), ...imageMetadata }; }), }); ``` ## Image Processing Integration **⚠️ Important:** Pushduck handles **file uploads only**. Image processing (resizing, optimization, format conversion) requires external tools. The examples below show **integration patterns** using `.onUploadComplete()` hooks. **Popular image processing tools:** * **Sharp** - Fast Node.js processing (recommended) * **Cloudinary** - Full-service image CDN with transformations * **Imgix** - Real-time URL-based transformations See [Philosophy](/docs/philosophy#what-pushduck-doesnt-do) for why we don't include processing. ### Integration Example: Sharp for Resizing **⚠️ Bandwidth Tradeoff:** Server-side processing requires **downloading** the file from S3 to your server (inbound bandwidth), processing it, then **uploading** variants back (outbound bandwidth). **This negates Pushduck's "server never touches files" benefit!** **Better Alternatives:** * **Client-side preprocessing** - Resize before upload (see below) * **URL-based processing** - Cloudinary/Imgix transform via URL (no download) * **Async queue** - Process in background worker, not blocking upload ```typescript // First: npm install sharp import sharp from 'sharp'; const s3Router = s3.createRouter({ optimizedImages: s3.image() .maxFileSize('15MB') .maxFiles(10) .accept(['image/jpeg', 'image/png', 'image/webp']) .dimensions({ maxWidth: 1920, maxHeight: 1080 }) .onUploadComplete(async ({ file, url, metadata }) => { // ⚠️ This downloads the file from S3 (inbound bandwidth) const imageBuffer = await fetch(url).then(r => r.arrayBuffer()); // Process with Sharp (external tool) const variants = await Promise.all([ sharp(imageBuffer).resize(150, 150, { fit: 'cover' }).toBuffer(), sharp(imageBuffer).resize(800, 600, { fit: 'inside' }).toBuffer(), sharp(imageBuffer).resize(1920, 1080, { fit: 'inside' }).toBuffer(), ]); // ⚠️ Upload variants back to S3 (outbound bandwidth) // await uploadVariantsToS3(variants); }), }); ``` *** ## Better Alternative: Client-Side Preprocessing **✅ Recommended Approach:** Process images **before** upload on the client side. This maintains Pushduck's "server never touches files" architecture and saves bandwidth. ### Client-Side Resize Before Upload ```typescript // Use browser-image-compression for client-side resizing // npm install browser-image-compression import imageCompression from 'browser-image-compression'; import { upload } from '@/lib/upload-client'; export function ClientSideImageUpload() { const { uploadFiles, isUploading } = upload.profilePicture(); const handleImageSelect = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // ✅ Resize on client BEFORE upload (no server bandwidth) const options = { maxSizeMB: 1, maxWidthOrHeight: 1920, useWebWorker: true, }; try { const compressedFile = await imageCompression(file, options); // Upload the already-processed file await uploadFiles([compressedFile]); console.log('Original:', file.size / 1024, 'KB'); console.log('Compressed:', compressedFile.size / 1024, 'KB'); } catch (error) { console.error('Compression error:', error); } }; return ( ); } ``` **Benefits:** * ✅ **No server bandwidth** - file never touches your server * ✅ **Faster uploads** - smaller files upload quicker * ✅ **Lower S3 costs** - store smaller files * ✅ **Edge compatible** - no Node.js processing * ✅ **Better UX** - instant preview of processed image *** ## Alternative: URL-Based Processing (Cloudinary/Imgix) **✅ Best of Both Worlds:** Upload original to S3, transform via URL without downloading. Zero server bandwidth! ```typescript const s3Router = s3.createRouter({ images: s3.image() .maxFileSize('10MB') .onUploadComplete(async ({ file, url, metadata }) => { // Save original URL - NO download needed await db.images.create({ userId: metadata.userId, originalUrl: url, s3Key: file.key, }); // ✅ Cloudinary can fetch from your S3 URL and transform // No bandwidth on your server! }), }); // Client: Use Cloudinary URLs for transformations function ImageDisplay({ s3Url }: { s3Url: string }) { // Cloudinary fetches from S3 and transforms (their bandwidth, not yours) const cloudinaryUrl = `https://res.cloudinary.com/your-cloud/image/fetch/w_400,h_400,c_fill/${encodeURIComponent(s3Url)}`; return Transformed; } ``` *** ## Advanced Patterns (Optional) ### Responsive Image Generation ```typescript interface ImageVariant { name: string; width: number; height?: number; quality?: number; format?: "jpeg" | "png" | "webp"; } const imageVariants: ImageVariant[] = [ { name: "thumbnail", width: 150, height: 150, quality: 80 }, { name: "small", width: 400, quality: 85 }, { name: "medium", width: 800, quality: 85 }, { name: "large", width: 1200, quality: 85 }, { name: "xlarge", width: 1920, quality: 90 }, ]; const s3Router = s3.createRouter({ responsiveImages: s3.image() .maxFileSize('20MB') .maxFiles(5) .accept(['image/jpeg', 'image/png', 'image/webp']) .onUploadComplete(async ({ file, url, metadata }) => { // Generate responsive variants const variants = await Promise.all( imageVariants.map((variant) => generateImageVariant(file, variant)) ); // Save variant information to database await saveImageVariants(file.key, variants, metadata.userId); }), }); // Client-side responsive image component export function ResponsiveImage({ src, alt, sizes = "(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw", }: { src: string; alt: string; sizes?: string; }) { const variants = useImageVariants(src); if (!variants) return {alt}; const srcSet = [ `${variants.small} 400w`, `${variants.medium} 800w`, `${variants.large} 1200w`, `${variants.xlarge} 1920w`, ].join(", "); return ( {alt} ); } ``` ### Image Upload with Crop & Preview ```typescript import { useState } from 'react' import { ImageCropper } from './image-cropper' import { upload } from '@/lib/upload-client' export function ImageUploadWithCrop() { const [selectedFile, setSelectedFile] = useState(null) const [croppedImage, setCroppedImage] = useState(null) const { uploadFiles, isUploading } = upload.profilePicture const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) setSelectedFile(file) } const handleCropComplete = (croppedBlob: Blob) => { setCroppedImage(croppedBlob) } const handleUpload = async () => { if (!croppedImage) return const file = new File([croppedImage], 'cropped-image.jpg', { type: 'image/jpeg' }) await uploadFiles([file]) // Reset state setSelectedFile(null) setCroppedImage(null) } return (
{!selectedFile && ( )} {selectedFile && !croppedImage && ( )} {croppedImage && (
Cropped preview
)}
) } ```
```typescript import { useRef, useCallback } from 'react' import ReactCrop, { Crop, PixelCrop } from 'react-image-crop' import 'react-image-crop/dist/ReactCrop.css' interface ImageCropperProps { image: File aspectRatio?: number onCropComplete: (croppedBlob: Blob) => void } export function ImageCropper({ image, aspectRatio = 1, onCropComplete }: ImageCropperProps) { const imgRef = useRef(null) const [crop, setCrop] = useState({ unit: '%', x: 25, y: 25, width: 50, height: 50 }) const imageUrl = URL.createObjectURL(image) const getCroppedImage = useCallback(async ( image: HTMLImageElement, crop: PixelCrop ): Promise => { const canvas = document.createElement('canvas') const ctx = canvas.getContext('2d')! const scaleX = image.naturalWidth / image.width const scaleY = image.naturalHeight / image.height canvas.width = crop.width * scaleX canvas.height = crop.height * scaleY ctx.imageSmoothingQuality = 'high' ctx.drawImage( image, crop.x * scaleX, crop.y * scaleY, crop.width * scaleX, crop.height * scaleY, 0, 0, canvas.width, canvas.height ) return new Promise(resolve => { canvas.toBlob(blob => resolve(blob!), 'image/jpeg', 0.9) }) }, []) const handleCropComplete = useCallback(async (crop: PixelCrop) => { if (imgRef.current && crop.width && crop.height) { const croppedBlob = await getCroppedImage(imgRef.current, crop) onCropComplete(croppedBlob) } }, [getCroppedImage, onCropComplete]) return (
Crop preview
) } ```
```typescript // Server-side image processing after upload const s3Router = s3.createRouter({ profilePicture: s3.image() .maxFileSize('10MB') .maxFiles(1) .accept(['image/jpeg', 'image/png', 'image/webp']) .onUploadComplete(async ({ file, url, metadata }) => { // Generate avatar sizes await Promise.all([ generateImageVariant(file, { name: 'avatar-small', width: 32, height: 32, fit: 'cover', quality: 90 }), generateImageVariant(file, { name: 'avatar-medium', width: 64, height: 64, fit: 'cover', quality: 90 }), generateImageVariant(file, { name: 'avatar-large', width: 128, height: 128, fit: 'cover', quality: 95 }) ]) // Update user profile with new avatar await updateUserAvatar(metadata.userId, { original: url, small: getVariantUrl(file.key, 'avatar-small'), medium: getVariantUrl(file.key, 'avatar-medium'), large: getVariantUrl(file.key, 'avatar-large') }) }) }) ```
## Image Upload Patterns ### Drag & Drop Image Gallery ```typescript import { useDropzone } from "react-dropzone"; import { upload } from "@/lib/upload-client"; export function ImageGalleryUploader() { const { uploadFiles, files, isUploading } = upload.galleryImages; const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: { "image/*": [".jpeg", ".jpg", ".png", ".webp", ".gif"], }, maxFiles: 10, onDrop: (acceptedFiles) => { uploadFiles(acceptedFiles); }, }); const removeFile = (fileId: string) => { // Implementation to remove file from gallery }; return (
{isDragActive ? (

Drop the images here...

) : (

Drag & drop images here, or click to select

Up to 10 images, max 10MB each

)}
{files.length > 0 && (
{files.map((file) => (
{file.status === "success" && (
{file.name}
)} {file.status === "uploading" && (
{file.progress}%

{file.name}

)} {file.status === "error" && (
⚠️

Upload failed

)}
))}
)}
); } ``` ### Image Upload with Metadata ```typescript const s3Router = s3.createRouter({ portfolioImages: s3.image() .maxFileSize('15MB') .maxFiles(20) .accept(['image/jpeg', 'image/png', 'image/webp']) .middleware(async ({ req, file, metadata }) => { const { userId } = await authenticateUser(req); // Extract and validate metadata const imageMetadata = await extractImageMetadata(file); // Return enriched metadata return { ...metadata, userId, uploadedBy: userId, uploadedAt: new Date(), originalFilename: file.name, fileHash: await calculateFileHash(file), ...imageMetadata, }; }) .onUploadComplete(async ({ file, url, metadata }) => { // Save detailed image information await saveImageToDatabase({ userId: metadata.userId, s3Key: file.key, url: url, filename: metadata.originalFilename, size: file.size, dimensions: { width: metadata.width, height: metadata.height, }, format: metadata.format, colorProfile: metadata.colorProfile, hasTransparency: metadata.hasTransparency, exifData: metadata.exif, hash: metadata.fileHash, }); }), }); ``` ## Performance Best Practices ```typescript import { compress } from 'image-conversion' export function optimizeImage(file: File): Promise { return compress(file, { quality: 0.8, type: 'image/webp', width: 1920, height: 1080, orientation: true // Auto-rotate based on EXIF }) } // Usage in upload component const handleFileSelect = async (files: File[]) => { const optimizedFiles = await Promise.all( files.map(file => optimizeImage(file)) ) uploadFiles(optimizedFiles) } ``` ```typescript export function ProgressiveImage({ src, blurDataURL, alt }: { src: string blurDataURL: string alt: string }) { const [isLoaded, setIsLoaded] = useState(false) return (
{alt} {alt} setIsLoaded(true)} />
) } ```
```typescript import { useIntersectionObserver } from '@/hooks/use-intersection-observer' export function LazyImage({ src, alt, ...props }) { const [ref, isIntersecting] = useIntersectionObserver({ threshold: 0.1, rootMargin: '50px' }) return (
{isIntersecting ? ( {alt} ) : (
Loading...
)}
) } ```
## Project Structure *** **Image Excellence**: With proper optimization, validation, and processing, your image uploads will provide an excellent user experience while maintaining performance and quality. # Guides & Tutorials (/docs/guides) import { Card, Cards } from "fumadocs-ui/components/card"; import { Callout } from "fumadocs-ui/components/callout"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Learning Path & Tutorials Learn how to build robust file upload features with pushduck through comprehensive guides covering everything from basic uploads to advanced production patterns. **Progressive Learning**: These guides are organized from basic concepts to advanced patterns. Start with client approaches and work your way up to production deployment. ## Getting Started * Hook-based vs Property-based clients * When to use each approach * Migration strategies * Performance considerations **Perfect for**: Understanding client patterns ## Upload Patterns * Image validation and processing * Automatic resizing and optimization * Format conversion * Progressive loading patterns **Perfect for**: Photo sharing, profile pictures, galleries ## Security & Authentication * User authentication strategies * Role-based access control * JWT integration * Session management **Essential for**: Secure applications * CORS setup for different providers * Access Control Lists (ACL) * Public vs private uploads * Security best practices **Essential for**: Production deployments ## Migration & Upgrades * Step-by-step migration process * Breaking changes and compatibility * Performance improvements * Type safety enhancements **Perfect for**: Upgrading existing projects ## Production Deployment * Environment configuration * Security considerations * Performance optimization * Monitoring and logging **Essential for**: Going live safely ## Common Patterns ### Basic Upload Flow **Configure Server Router** Set up your upload routes with validation: ```typescript const uploadRouter = createS3Router({ routes: { imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB"), }, }); ``` **Implement Client Upload** Use hooks or client for reactive uploads: ```typescript const { upload, uploading, progress } = useUpload({ endpoint: '/api/upload', route: 'imageUpload', }); ``` **Handle Upload Results** Process successful uploads and errors: ```typescript const result = await upload(file); console.log('File uploaded:', result.url); ``` ### Authentication Pattern ```typescript // Server: Add authentication middleware const uploadRouter = createS3Router({ middleware: [ async (req) => { const user = await authenticate(req); if (!user) throw new Error('Unauthorized'); return { user }; } ], routes: { userAvatar: s3.image() .maxFileSize("2MB") .path(({ metadata }) => `avatars/${metadata.user.id}`), }, }); // Client: Include auth headers const { upload } = useUpload({ endpoint: '/api/upload', route: 'userAvatar', headers: { Authorization: `Bearer ${token}`, }, }); ``` ## Architecture Patterns ### Multi-Provider Setup ```typescript // Support multiple storage providers const uploadRouter = createS3Router({ storage: process.env.NODE_ENV === 'production' ? { provider: 'aws-s3', ... } : { provider: 'minio', ... }, routes: { // Your routes remain the same }, }); ``` ### Route-Based Organization ```typescript const uploadRouter = createS3Router({ routes: { // Public uploads publicImages: s3.image().maxFileSize("5MB").public(), // User-specific uploads userDocuments: s3.file() .maxFileSize("10MB") .path(({ metadata }) => `users/${metadata.userId}/documents`), // Admin uploads adminAssets: s3.file() .maxFileSize("50MB") .middleware([requireAdmin]), }, }); ``` ## Performance Tips **Optimization Strategies**: * Use appropriate file size limits for your use case * Implement client-side validation before upload * Consider using presigned URLs for large files * Enable CDN for frequently accessed files * Implement progressive upload for large files ## Troubleshooting Quick Links | Issue | Solution | | ----------------- | -------------------------------------------------------------------------- | | **CORS errors** | Check [CORS Configuration](/docs/guides/security/cors-and-acl) | | **Auth failures** | Review [Authentication Guide](/docs/guides/security/authentication) | | **Slow uploads** | See [Production Checklist](/docs/guides/production-checklist) | | **Type errors** | Check [Enhanced Client Migration](/docs/guides/migrate-to-enhanced-client) | ## What's Next? 1. **New to pushduck?** → Start with [Client Approaches](/docs/guides/client-approaches) 2. **Building image features?** → Check [Image Uploads](/docs/guides/image-uploads) 3. **Adding security?** → Review [Authentication](/docs/guides/security/authentication) 4. **Going to production?** → Use [Production Checklist](/docs/guides/production-checklist) 5. **Need help?** → Visit our [troubleshooting guide](/docs/api/troubleshooting) **Community Guides**: Have a useful pattern or solution? Consider contributing to our documentation to help other developers! # Enhanced Client Migration (/docs/guides/migrate-to-enhanced-client) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Migrating to Enhanced Client Upgrade to the new property-based client API for enhanced type safety, better developer experience, and elimination of string literals. The enhanced client API is **100% backward compatible**. You can migrate gradually without breaking existing code. ## Why Migrate? ```typescript // ❌ Old: String literals, no type safety const {uploadFiles} = useUploadRoute("imageUpload") // ✅ New: Property-based, full type inference const {uploadFiles} = upload.imageUpload ``` ```typescript // ✅ Autocomplete shows all your endpoints upload. // imageUpload, documentUpload, videoUpload... // ^ No more guessing endpoint names ``` ```typescript // When you rename routes in your router, // TypeScript shows errors everywhere they're used // Making refactoring safe and easy ``` ## Migration Steps **Install Latest Version** Ensure you're using the latest version of pushduck: ```bash npm install pushduck@latest ``` ```bash yarn add pushduck@latest ``` ```bash pnpm add pushduck@latest ``` ```bash bun add pushduck@latest ``` **Create Upload Client** Set up your typed upload client: ```typescript title="lib/upload-client.ts" import { createUploadClient } from 'pushduck/client' import type { AppRouter } from './upload' // Your router type export const upload = createUploadClient({ endpoint: '/api/upload' }) ``` **Migrate Components Gradually** Update your components one by one: ```typescript import { useUploadRoute } from 'pushduck/client' export function ImageUploader() { const { uploadFiles, files, isUploading } = useUploadRoute('imageUpload') return (
uploadFiles(e.target.files)} /> {/* Upload UI */}
) } ```
```typescript import { upload } from '@/lib/upload-client' export function ImageUploader() { const { uploadFiles, files, isUploading } = upload.imageUpload return (
uploadFiles(e.target.files)} /> {/* Same upload UI */}
) } ```
**Update Imports** Once migrated, you can remove old hook imports: ```typescript // Remove old imports // import { useUploadRoute } from 'pushduck/client' // Use new client import import { upload } from '@/lib/upload-client' ```
## Migration Examples ### Basic Component Migration ```typescript import { useUploadRoute } from 'pushduck/client' export function DocumentUploader() { const { uploadFiles, files, isUploading, error, reset } = useUploadRoute('documentUpload', { onSuccess: (results) => { console.log('Uploaded:', results) }, onError: (error) => { console.error('Error:', error) } }) return (
uploadFiles(Array.from(e.target.files || []))} disabled={isUploading} /> {files.map(file => (
{file.name}
))} {error &&
Error: {error.message}
}
) } ```
```typescript import { upload } from '@/lib/upload-client' export function DocumentUploader() { const { uploadFiles, files, isUploading, error, reset } = upload.documentUpload // Handle callbacks with upload options const handleUpload = async (selectedFiles: File[]) => { try { const results = await uploadFiles(selectedFiles) console.log('Uploaded:', results) } catch (error) { console.error('Error:', error) } } return (
handleUpload(Array.from(e.target.files || []))} disabled={isUploading} /> {files.map(file => (
{file.name}
))} {error &&
Error: {error.message}
}
) } ```
### Form Integration Migration ```typescript import { useForm } from 'react-hook-form' import { useUploadRoute } from 'pushduck/client' export function ProductForm() { const { register, handleSubmit, setValue } = useForm() const { uploadFiles, uploadedFiles } = useUploadRoute('productImages', { onSuccess: (results) => { setValue('images', results.map(r => r.url)) } }) return (
uploadFiles(Array.from(e.target.files || []))} />
) } ```
```typescript import { useForm } from 'react-hook-form' import { upload } from '@/lib/upload-client' export function ProductForm() { const { register, handleSubmit, setValue } = useForm() const { uploadFiles } = upload.productImages const handleImageUpload = async (files: File[]) => { const results = await uploadFiles(files) setValue('images', results.map(r => r.url)) } return (
handleImageUpload(Array.from(e.target.files || []))} />
) } ```
### Multiple Upload Types Migration ```typescript export function MediaUploader() { const images = useUploadRoute('imageUpload') const videos = useUploadRoute('videoUpload') const documents = useUploadRoute('documentUpload') return (

Images

images.uploadFiles(e.target.files)} />

Videos

videos.uploadFiles(e.target.files)} />

Documents

documents.uploadFiles(e.target.files)} />
) } ```
```typescript import { upload } from '@/lib/upload-client' export function MediaUploader() { const images = upload.imageUpload const videos = upload.videoUpload const documents = upload.documentUpload return (

Images

images.uploadFiles(e.target.files)} />

Videos

videos.uploadFiles(e.target.files)} />

Documents

documents.uploadFiles(e.target.files)} />
) } ```
## Key Differences ### API Comparison | Feature | Hook-Based API | Property-Based API | | ------------------ | ------------------------- | --------------------------- | | **Type Safety** | Runtime string validation | Compile-time type checking | | **IntelliSense** | Limited autocomplete | Full endpoint autocomplete | | **Refactoring** | Manual find/replace | Automatic TypeScript errors | | **Bundle Size** | Slightly larger | Optimized tree-shaking | | **Learning Curve** | Familiar React pattern | New property-based pattern | ### Callback Handling ```typescript const { uploadFiles } = useUploadRoute('images', { onSuccess: (results) => console.log('Success:', results), onError: (error) => console.error('Error:', error), onProgress: (progress) => console.log('Progress:', progress) }) ``` ```typescript const { uploadFiles } = upload.images await uploadFiles(files, { onSuccess: (results) => console.log('Success:', results), onError: (error) => console.error('Error:', error), onProgress: (progress) => console.log('Progress:', progress) }) ``` ## Troubleshooting ### Common Migration Issues **Type Errors:** If you see TypeScript errors after migration, ensure your router type is properly exported and imported. ```typescript // ❌ Missing router type export const upload = createUploadClient({ endpoint: "/api/upload", }); // ✅ With proper typing export const upload = createUploadClient({ endpoint: "/api/upload", }); ``` ### Gradual Migration Strategy You can use both APIs simultaneously during migration: ```typescript // Keep existing hook-based components working const hookUpload = useUploadRoute("imageUpload"); // Use new property-based API for new components const propertyUpload = upload.imageUpload; // Both work with the same backend! ``` ## Benefits After Migration * **🎯 Enhanced Type Safety**: Catch errors at compile time, not runtime * **🚀 Better Performance**: Optimized bundle size with tree-shaking * **💡 Improved DX**: Full IntelliSense support for all endpoints * **🔧 Safe Refactoring**: Rename endpoints without breaking your app * **📦 Future-Proof**: Built for the next generation of pushduck features *** **Migration Complete!** You now have enhanced type safety and a better developer experience. Need help? Join our [Discord community](https://pushduck.dev/discord) for support. # Production Checklist (/docs/guides/production-checklist) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Going Live Checklist Get your file uploads production-ready. Start with the 8 essentials below—most apps don't need more than this. **Quick Path to Production:** Complete the essential checklist below (8 items) and you're ready to deploy. Advanced optimizations can be added later as you scale. ## Essential Checklist (Required) These 8 items are critical for safe production deployment: **1. Authentication** * [ ] Auth middleware on all upload routes * [ ] Unauthenticated requests are blocked ```typescript const router = s3.createRouter({ userFiles: s3.image() .middleware(async ({ req }) => { const session = await getServerSession(req); if (!session) throw new Error("Auth required"); return { userId: session.user.id }; }) }); ``` **2. Environment Variables** * [ ] S3 credentials in `.env` (not in code) * [ ] Secrets are strong and unique ```bash AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx AWS_REGION=us-east-1 S3_BUCKET_NAME=your-bucket ``` **3. File Validation** * [ ] File type restrictions (`.accept()`) * [ ] File size limits (`.maxFileSize()`) ```typescript userPhotos: s3.image() .maxFileSize("10MB") .maxFiles(5) .accept(["image/jpeg", "image/png", "image/webp"]) ``` **4. CORS Configuration** * [ ] CORS set up on S3 bucket * [ ] Only your domain is allowed See [CORS Setup Guide](/docs/guides/security/cors-and-acl) **5. Error Monitoring** * [ ] Error tracking enabled (Sentry/LogRocket) * [ ] Upload failures are logged ```typescript .onUploadError(async ({ error }) => { console.error('Upload failed:', error); // Sentry.captureException(error); }) ``` **6. Basic Rate Limiting** (Optional but recommended) * [ ] Prevent abuse with upload limits Use Upstash or Vercel KV for simple rate limiting. **7. Test Uploads** * [ ] Upload works in production environment * [ ] Files appear in S3 bucket correctly * [ ] URLs are accessible **8. Backup Strategy** * [ ] S3 versioning enabled (optional) * [ ] Know how to restore deleted files **✅ Done!** If you've completed these 8 items, your upload system is production-ready. *** ## When You Need More **Most apps are production-ready with the 8 essentials above.** As you scale, consider: * **CDN integration** - For global audience or high traffic * **Advanced auth** - RBAC/ABAC for enterprise permissions (see [Authentication Guide](/docs/guides/security/authentication)) * **Redis caching** - For 10k+ requests/minute * **Multi-region** - For mission-critical redundancy *** ## Next Steps Deep dive into authentication patterns Configure CORS for your provider Common issues and solutions # Astro (/docs/integrations/astro) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; **🚧 Client-Side In Development**: Astro server-side integration is fully functional with Web Standards APIs. However, Astro-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now. ## Using pushduck with Astro Astro is a modern web framework for building fast, content-focused websites with islands architecture. It uses Web Standards APIs and provides excellent performance with minimal JavaScript. Since Astro uses standard `Request`/`Response` objects, pushduck handlers work directly without any adapters! **Web Standards Native**: Astro API routes use Web Standard `Request`/`Response` objects, making pushduck integration straightforward with zero overhead. ## Quick Setup **Install dependencies** ```bash npm install pushduck ``` **Configure upload router** ```typescript title="src/lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: import.meta.env.AWS_ACCESS_KEY_ID!, secretAccessKey: import.meta.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: import.meta.env.AWS_ENDPOINT_URL!, bucket: import.meta.env.S3_BUCKET_NAME!, accountId: import.meta.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create API route** ```typescript title="src/pages/api/upload/[...path].ts" import type { APIRoute } from 'astro'; import { uploadRouter } from '../../../lib/upload'; // Direct usage - no adapter needed! export const ALL: APIRoute = async ({ request }) => { return uploadRouter.handlers(request); }; ``` ## Basic Integration ### Simple Upload Route ```typescript title="src/pages/api/upload/[...path].ts" import type { APIRoute } from 'astro'; import { uploadRouter } from '../../../lib/upload'; // Method 1: Combined handler (recommended) export const ALL: APIRoute = async ({ request }) => { return uploadRouter.handlers(request); }; // Method 2: Separate handlers (if you need method-specific logic) export const GET: APIRoute = async ({ request }) => { return uploadRouter.handlers.GET(request); }; export const POST: APIRoute = async ({ request }) => { return uploadRouter.handlers.POST(request); }; ``` ### With CORS Support ```typescript title="src/pages/api/upload/[...path].ts" import type { APIRoute } from 'astro'; import { uploadRouter } from '../../../lib/upload'; export const ALL: APIRoute = async ({ request }) => { // Handle CORS preflight if (request.method === 'OPTIONS') { return new Response(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }, }); } const response = await uploadRouter.handlers(request); // Add CORS headers to actual response response.headers.set('Access-Control-Allow-Origin', '*'); return response; }; ``` ## Advanced Configuration ### Authentication with Astro ```typescript title="src/lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: import.meta.env.AWS_ACCESS_KEY_ID!, secretAccessKey: import.meta.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: import.meta.env.AWS_ENDPOINT_URL!, bucket: import.meta.env.S3_BUCKET_NAME!, accountId: import.meta.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with cookie-based authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const cookies = req.headers.get('Cookie'); const sessionId = parseCookie(cookies)?.sessionId; if (!sessionId) { throw new Error('Authentication required'); } const user = await getUserFromSession(sessionId); if (!user) { throw new Error('Invalid session'); } return { userId: user.id, username: user.username, }; }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; // Helper functions function parseCookie(cookieString: string | null) { if (!cookieString) return {}; return Object.fromEntries( cookieString.split('; ').map(c => { const [key, ...v] = c.split('='); return [key, v.join('=')]; }) ); } async function getUserFromSession(sessionId: string) { // Implement your session validation logic // This could connect to a database, Redis, etc. return { id: 'user-123', username: 'demo-user' }; } ``` ## Client-Side Usage ### Upload Component (React) ```tsx title="src/components/FileUpload.tsx" import { useUpload } from "pushduck/client"; import type { AppUploadRouter } from "../lib/upload"; const { UploadButton, UploadDropzone } = useUpload({ endpoint: "/api/upload", }); export default function FileUpload() { function handleUploadComplete(files: any[]) { console.log("Files uploaded:", files); alert("Upload completed!"); } function handleUploadError(error: Error) { console.error("Upload error:", error); alert(`Upload failed: ${error.message}`); } return (

Image Upload

Document Upload

); } ``` ### Upload Component (Vue) ```vue title="src/components/FileUpload.vue" ``` ### Using in Astro Pages ```astro title="src/pages/index.astro" --- // Server-side code (runs at build time) --- File Upload Demo

File Upload Demo

``` ## File Management ### Server-Side File API ```typescript title="src/pages/api/files.ts" import type { APIRoute } from 'astro'; export const GET: APIRoute = async ({ request, url }) => { const searchParams = url.searchParams; const userId = searchParams.get('userId'); if (!userId) { return new Response(JSON.stringify({ error: 'User ID required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // Fetch files from database const files = await getFilesForUser(userId); return new Response(JSON.stringify({ files: files.map(file => ({ id: file.id, name: file.name, url: file.url, size: file.size, uploadedAt: file.createdAt, })), }), { headers: { 'Content-Type': 'application/json' } }); }; async function getFilesForUser(userId: string) { // Implement your database query logic return []; } ``` ### File Management Page ```astro title="src/pages/files.astro" --- // This runs on the server at build time or request time const files = await fetch(`${Astro.url.origin}/api/files?userId=current-user`) .then(res => res.json()) .catch(() => ({ files: [] })); --- My Files

My Files

Uploaded Files

{files.files.length === 0 ? (

No files uploaded yet.

) : (
{files.files.map((file: any) => (

{file.name}

{formatFileSize(file.size)}

{new Date(file.uploadedAt).toLocaleDateString()}

View File
))}
)}
``` ## Deployment Options ```javascript title="astro.config.mjs" import { defineConfig } from 'astro/config'; import vercel from '@astrojs/vercel/serverless'; export default defineConfig({ output: 'server', adapter: vercel({ runtime: 'nodejs18.x', }), }); ``` ```javascript title="astro.config.mjs" import { defineConfig } from 'astro/config'; import netlify from '@astrojs/netlify/functions'; export default defineConfig({ output: 'server', adapter: netlify(), }); ``` ```javascript title="astro.config.mjs" import { defineConfig } from 'astro/config'; import node from '@astrojs/node'; export default defineConfig({ output: 'server', adapter: node({ mode: 'standalone', }), }); ``` ```javascript title="astro.config.mjs" import { defineConfig } from 'astro/config'; import cloudflare from '@astrojs/cloudflare'; export default defineConfig({ output: 'server', adapter: cloudflare(), }); ``` ## Environment Variables ```bash title=".env" # AWS Configuration AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_S3_BUCKET=your-bucket-name # Astro PUBLIC_UPLOAD_ENDPOINT=http://localhost:3000/api/upload ``` ## Performance Benefits ## Real-Time Upload Progress ```tsx title="src/components/AdvancedUpload.tsx" import { useState } from 'react'; export default function AdvancedUpload() { const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); async function handleFileUpload(event: React.ChangeEvent) { const files = event.target.files; if (!files || files.length === 0) return; setIsUploading(true); setUploadProgress(0); try { // Simulate upload progress for (let i = 0; i <= 100; i += 10) { setUploadProgress(i); await new Promise(resolve => setTimeout(resolve, 100)); } alert('Upload completed!'); } catch (error) { console.error('Upload failed:', error); alert('Upload failed!'); } finally { setIsUploading(false); setUploadProgress(0); } } return (
{isUploading && (

{uploadProgress}% uploaded

)}
); } ``` ## Troubleshooting **Common Issues** 1. **Route not found**: Ensure your route is `src/pages/api/upload/[...path].ts` 2. **Build errors**: Check that pushduck is properly installed and configured 3. **Environment variables**: Use `import.meta.env` instead of `process.env` 4. **Client components**: Remember to add `client:load` directive for interactive components ### Debug Mode Enable debug logging: ```typescript title="src/lib/upload.ts" export const uploadRouter = createS3Router({ // ... routes }).middleware(async ({ req, file }) => { if (import.meta.env.DEV) { console.log("Upload request:", req.url); console.log("File:", file.name, file.size); } return {}; }); ``` ### Astro Configuration ```javascript title="astro.config.mjs" import { defineConfig } from 'astro/config'; import react from '@astrojs/react'; import vue from '@astrojs/vue'; export default defineConfig({ integrations: [ react(), // For React components vue(), // For Vue components ], output: 'server', // Required for API routes vite: { define: { // Make environment variables available 'import.meta.env.AWS_ACCESS_KEY_ID': JSON.stringify(process.env.AWS_ACCESS_KEY_ID), } } }); ``` Astro provides an excellent foundation for building fast, content-focused websites with pushduck, combining the power of islands architecture with Web Standards APIs for optimal performance and developer experience. # Bun Runtime (/docs/integrations/bun) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Using pushduck with Bun Bun is an ultra-fast JavaScript runtime with native Web Standards support. Since Bun uses Web Standard `Request` and `Response` objects natively, pushduck handlers work directly without any adapters! **Web Standards Native**: Bun's `Bun.serve()` uses Web Standard `Request` objects directly, making pushduck integration straightforward with zero overhead. ## Quick Setup **Install dependencies** ```bash bun add pushduck ``` ```bash npm install pushduck ``` ```bash yarn add pushduck ``` ```bash pnpm add pushduck ``` **Configure upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create Bun server with upload routes** ```typescript title="server.ts" import { uploadRouter } from './lib/upload'; // Direct usage - no adapter needed! Bun.serve({ port: 3000, fetch(request) { const url = new URL(request.url); if (url.pathname.startsWith('/api/upload/')) { return uploadRouter.handlers(request); } return new Response('Not found', { status: 404 }); }, }); console.log('Bun server running on http://localhost:3000'); ``` ## Basic Integration ### Simple Upload Server ```typescript title="server.ts" import { uploadRouter } from './lib/upload'; Bun.serve({ port: 3000, fetch(request) { const url = new URL(request.url); // Method 1: Combined handler (recommended) if (url.pathname.startsWith('/api/upload/')) { return uploadRouter.handlers(request); } // Health check if (url.pathname === '/health') { return new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' } }); } return new Response('Not found', { status: 404 }); }, }); console.log('Bun server running on http://localhost:3000'); ``` ### With CORS and Routing ```typescript title="server.ts" import { uploadRouter } from './lib/upload'; function handleCORS(request: Request) { const origin = request.headers.get('origin'); const allowedOrigins = ['http://localhost:3000', 'https://your-domain.com']; const headers = new Headers(); if (origin && allowedOrigins.includes(origin)) { headers.set('Access-Control-Allow-Origin', origin); } headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); return headers; } Bun.serve({ port: 3000, fetch(request) { const url = new URL(request.url); const corsHeaders = handleCORS(request); // Handle preflight requests if (request.method === 'OPTIONS') { return new Response(null, { status: 200, headers: corsHeaders }); } // Upload routes if (url.pathname.startsWith('/api/upload/')) { return uploadRouter.handlers(request).then(response => { // Add CORS headers to response corsHeaders.forEach((value, key) => { response.headers.set(key, value); }); return response; }); } // Health check if (url.pathname === '/health') { return new Response(JSON.stringify({ status: 'ok', runtime: 'Bun', timestamp: new Date().toISOString() }), { headers: { 'Content-Type': 'application/json', ...Object.fromEntries(corsHeaders) } }); } return new Response('Not found', { status: 404 }); }, }); console.log('Bun server running on http://localhost:3000'); ``` ## Advanced Configuration ### Authentication and Rate Limiting ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const authHeader = req.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authorization required'); } const token = authHeader.substring(7); try { const payload = await verifyJWT(token); return { userId: payload.sub as string, userRole: payload.role as string }; } catch (error) { throw new Error('Invalid token'); } }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); async function verifyJWT(token: string) { // Your JWT verification logic here // Using Bun's built-in crypto or a JWT library return { sub: 'user-123', role: 'user' }; } export type AppUploadRouter = typeof uploadRouter; ``` ### Production Server with Full Features ```typescript title="server.ts" import { uploadRouter } from './lib/upload'; // Simple rate limiting store const rateLimitStore = new Map(); function rateLimit(ip: string, maxRequests = 100, windowMs = 15 * 60 * 1000) { const now = Date.now(); const key = ip; const record = rateLimitStore.get(key); if (!record || now > record.resetTime) { rateLimitStore.set(key, { count: 1, resetTime: now + windowMs }); return true; } if (record.count >= maxRequests) { return false; } record.count++; return true; } function getClientIP(request: Request): string { // In production, you might get this from headers like X-Forwarded-For return request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'; } Bun.serve({ port: process.env.PORT ? parseInt(process.env.PORT) : 3000, fetch(request) { const url = new URL(request.url); const clientIP = getClientIP(request); // Rate limiting if (!rateLimit(clientIP)) { return new Response(JSON.stringify({ error: 'Too many requests' }), { status: 429, headers: { 'Content-Type': 'application/json' } }); } // CORS const corsHeaders = { 'Access-Control-Allow-Origin': process.env.NODE_ENV === 'production' ? 'https://your-domain.com' : '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }; // Handle preflight if (request.method === 'OPTIONS') { return new Response(null, { status: 200, headers: corsHeaders }); } // Upload routes if (url.pathname.startsWith('/api/upload/')) { return uploadRouter.handlers(request).then(response => { Object.entries(corsHeaders).forEach(([key, value]) => { response.headers.set(key, value); }); return response; }).catch(error => { console.error('Upload error:', error); return new Response(JSON.stringify({ error: 'Upload failed', message: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json', ...corsHeaders } }); }); } // API info if (url.pathname === '/api') { return new Response(JSON.stringify({ name: 'Bun Upload API', version: '1.0.0', runtime: 'Bun', endpoints: { health: '/health', upload: '/api/upload/*' } }), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } // Health check if (url.pathname === '/health') { return new Response(JSON.stringify({ status: 'ok', runtime: 'Bun', version: Bun.version, timestamp: new Date().toISOString(), uptime: process.uptime() }), { headers: { 'Content-Type': 'application/json', ...corsHeaders } }); } return new Response('Not found', { status: 404, headers: corsHeaders }); }, }); console.log(`Bun server running on http://localhost:${process.env.PORT || 3000}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); ``` ## File-based Routing ### Structured Application ```typescript title="routes/upload.ts" import { uploadRouter } from '../lib/upload'; export function handleUpload(request: Request) { return uploadRouter.handlers(request); } ``` ```typescript title="routes/api.ts" export function handleAPI(request: Request) { return new Response(JSON.stringify({ name: 'Bun Upload API', version: '1.0.0', runtime: 'Bun' }), { headers: { 'Content-Type': 'application/json' } }); } ``` ```typescript title="server.ts" import { handleUpload } from './routes/upload'; import { handleAPI } from './routes/api'; const routes = { '/api/upload': handleUpload, '/api': handleAPI, '/health': () => new Response(JSON.stringify({ status: 'ok' }), { headers: { 'Content-Type': 'application/json' } }) }; Bun.serve({ port: 3000, fetch(request) { const url = new URL(request.url); for (const [path, handler] of Object.entries(routes)) { if (url.pathname.startsWith(path)) { return handler(request); } } return new Response('Not found', { status: 404 }); }, }); ``` ## Performance Benefits Bun is 3x faster than Node.js, providing incredible performance for file upload operations. No adapter layer means zero performance overhead - pushduck handlers run directly in Bun. Built-in bundler, test runner, package manager, and more - no extra tooling needed. Run TypeScript directly without compilation, perfect for rapid development. ## Deployment ### Docker Deployment ```dockerfile title="Dockerfile" FROM oven/bun:1 as base WORKDIR /usr/src/app # Install dependencies COPY package.json bun.lockb ./ RUN bun install --frozen-lockfile # Copy source code COPY . . # Expose port EXPOSE 3000 # Run the app CMD ["bun", "run", "server.ts"] ``` ### Production Scripts ```json title="package.json" { "name": "bun-upload-server", "version": "1.0.0", "scripts": { "dev": "bun run --watch server.ts", "start": "bun run server.ts", "build": "bun build server.ts --outdir ./dist --target bun", "test": "bun test" }, "dependencies": { "pushduck": "latest" }, "devDependencies": { "bun-types": "latest" } } ``` *** **Bun + Pushduck**: The perfect combination for ultra-fast file uploads with zero configuration overhead and exceptional developer experience. # Elysia (/docs/integrations/elysia) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Using pushduck with Elysia Elysia is a TypeScript-first web framework designed for Bun. Since Elysia uses Web Standard `Request` objects natively, pushduck handlers work directly without any adapters! **Web Standards Native**: Elysia exposes `context.request` as a Web Standard `Request` object, making pushduck integration straightforward with zero overhead. ## Quick Setup **Install dependencies** ```bash bun add pushduck ``` ```bash npm install pushduck ``` ```bash yarn add pushduck ``` ```bash pnpm add pushduck ``` **Configure upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create Elysia app with upload routes** ```typescript title="server.ts" import { Elysia } from 'elysia'; import { uploadRouter } from './lib/upload'; const app = new Elysia(); // Direct usage - no adapter needed! app.get('/api/upload', ({ request }) => uploadRouter.handlers.GET(request)); app.post('/api/upload', ({ request }) => uploadRouter.handlers.POST(request)); app.listen(3000); ``` ## Basic Integration ### Simple Upload Route ```typescript title="server.ts" import { Elysia } from 'elysia'; import { uploadRouter } from './lib/upload'; const app = new Elysia(); // Separate GET and POST handlers (required) app.get('/api/upload', ({ request }) => uploadRouter.handlers.GET(request)); app.post('/api/upload', ({ request }) => uploadRouter.handlers.POST(request)); app.listen(3000); ``` ### With Middleware and CORS ```typescript title="server.ts" import { Elysia } from 'elysia'; import { cors } from '@elysiajs/cors'; import { uploadRouter } from './lib/upload'; const app = new Elysia() .use(cors({ origin: ['http://localhost:3000', 'https://your-domain.com'], allowedHeaders: ['Content-Type', 'Authorization'], methods: ['GET', 'POST'] })) // Upload routes .get('/api/upload', ({ request }) => uploadRouter.handlers.GET(request)) .post('/api/upload', ({ request }) => uploadRouter.handlers.POST(request)) // Health check .get('/health', () => ({ status: 'ok' })) .listen(3000); console.log(`🦊 Elysia is running at http://localhost:3000`); ``` ## Advanced Configuration ### Authentication with JWT ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; import jwt from '@elysiajs/jwt'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with JWT authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const authHeader = req.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authorization required'); } const token = authHeader.substring(7); try { // Use your JWT verification logic here const payload = jwt.verify(token, process.env.JWT_SECRET!); return { userId: payload.sub as string, userRole: payload.role as string }; } catch (error) { throw new Error('Invalid token'); } }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; ``` ### Full Production Setup ```typescript title="server.ts" import { Elysia } from 'elysia'; import { cors } from '@elysiajs/cors'; import { rateLimit } from '@elysiajs/rate-limit'; import { swagger } from '@elysiajs/swagger'; import { uploadRouter } from './lib/upload'; const app = new Elysia() // Swagger documentation .use(swagger({ documentation: { info: { title: 'Upload API', version: '1.0.0' } } })) // CORS .use(cors({ origin: process.env.NODE_ENV === 'production' ? ['https://your-domain.com'] : true, allowedHeaders: ['Content-Type', 'Authorization'], methods: ['GET', 'POST'] })) // Rate limiting .use(rateLimit({ max: 100, windowMs: 15 * 60 * 1000, // 15 minutes })) // Upload routes .get('/api/upload', ({ request }) => uploadRouter.handlers.GET(request)) .post('/api/upload', ({ request }) => uploadRouter.handlers.POST(request)) // Health check .get('/health', () => ({ status: 'ok', timestamp: new Date().toISOString() })) .listen(process.env.PORT || 3000); console.log(`🦊 Elysia is running at http://localhost:${process.env.PORT || 3000}`); ``` ## TypeScript Integration ### Type-Safe Client ```typescript title="lib/upload-client.ts" import { createUploadClient } from 'pushduck/client'; import type { AppUploadRouter } from './upload'; export const uploadClient = createUploadClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000' }); ``` ### Client Usage ```typescript title="components/upload.tsx" import { uploadClient } from '../lib/upload-client'; export function UploadComponent() { const handleUpload = async (files: File[]) => { try { const results = await uploadClient.upload('imageUpload', { files, // Type-safe metadata based on your router configuration metadata: { userId: 'user-123' } }); console.log('Upload successful:', results); } catch (error) { console.error('Upload failed:', error); } }; return ( { if (e.target.files) { handleUpload(Array.from(e.target.files)); } }} /> ); } ``` ## Performance Benefits No adapter layer means zero performance overhead - pushduck handlers run directly in Elysia. Built for Bun's exceptional performance, perfect for high-throughput upload APIs. Full TypeScript support from server to client with compile-time safety. Extensive plugin ecosystem for authentication, validation, rate limiting, and more. ## Deployment ### Production Deployment ```dockerfile title="Dockerfile" FROM oven/bun:1 as base WORKDIR /usr/src/app # Install dependencies COPY package.json bun.lockb ./ RUN bun install --frozen-lockfile # Copy source code COPY . . # Expose port EXPOSE 3000 # Run the app CMD ["bun", "run", "server.ts"] ``` ```bash # Build and run docker build -t my-upload-api . docker run -p 3000:3000 my-upload-api ``` *** **Perfect TypeScript Integration**: Elysia's TypeScript-first approach combined with pushduck's type-safe design creates an exceptional developer experience with full end-to-end type safety. # Expo & React Native (/docs/integrations/expo) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; ## Overview `pushduck/react-native` is a drop-in replacement for `pushduck/client` that works in React Native and Expo apps. The API is identical — `useUploadRoute` and `createUploadClient` work exactly the same way. The only difference is that `uploadFiles` accepts picker asset objects directly, so you never need to map field names. **Supported pickers out of the box:** * `expo-image-picker` — pass `result.assets` directly * `expo-document-picker` — pass `result.assets` directly * `react-native-image-picker` — filter for `uri` then pass directly The server side is unchanged — your existing upload route works without any modification. **`endpoint` must be an absolute URL.** React Native has no concept of a base URL, so relative paths like `/api/s3-upload` will throw a network error. Always pass the full URL: `https://your-api.com/api/s3-upload`. *** ## Setup **Install pushduck** ```bash npm install pushduck ``` ```bash yarn add pushduck ``` ```bash pnpm add pushduck ``` ```bash bun add pushduck ``` Install whichever picker(s) you need: ```bash npx expo install expo-image-picker npx expo install expo-document-picker # react-native-image-picker is installed via npm/yarn directly ``` **Create the upload client** Import from `pushduck/react-native` and provide an absolute `endpoint`: ```typescript title="lib/upload.ts" import { createUploadClient } from 'pushduck/react-native'; import type { AppRouter } from './upload-server'; // your server router type export const upload = createUploadClient({ endpoint: process.env.EXPO_PUBLIC_API_URL + '/api/s3-upload', // e.g. 'https://your-api.com/api/s3-upload' }); ``` Or use `useUploadRoute` directly in a component: ```typescript import { useUploadRoute } from 'pushduck/react-native'; const { uploadFiles, files, isUploading, progress } = useUploadRoute('imageUpload', { endpoint: 'https://your-api.com/api/s3-upload', }); ``` **Configure the server** The server route is identical to any other pushduck integration. See [Quick Start](/docs/quick-start) or the [Next.js](/docs/integrations/nextjs) / [Express](/docs/integrations/express) guide depending on your backend. ```typescript title="(your backend) lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3 } = createUploadConfig() .provider('cloudflareR2', { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = s3.createRouter({ imageUpload: s3.image().maxFileSize('10MB'), documentUpload: s3.file().maxFileSize('25MB'), }); export type AppRouter = typeof uploadRouter; ``` *** ## Usage by Picker ### expo-image-picker ```typescript title="components/ImageUploader.tsx" import React from 'react'; import { View, Text, TouchableOpacity, Image, Alert, Platform } from 'react-native'; import * as ImagePicker from 'expo-image-picker'; import { upload } from '../lib/upload'; export default function ImageUploader() { const { uploadFiles, files, isUploading, errors } = upload.imageUpload(); const pickAndUpload = async () => { if (Platform.OS !== 'web') { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { Alert.alert('Permission needed', 'Camera roll permission is required.'); return; } } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsMultipleSelection: true, }); if (!result.canceled) { // Pass result.assets directly — no mapping needed await uploadFiles(result.assets); } }; return ( {isUploading ? 'Uploading...' : 'Pick Images'} {errors.length > 0 && ( {errors[0]} )} {files.map((file) => ( {file.name} — {file.status} ({file.progress}%) {file.status === 'success' && file.url && ( )} ))} ); } ``` ### expo-document-picker ```typescript title="components/DocumentUploader.tsx" import React from 'react'; import { View, Text, TouchableOpacity } from 'react-native'; import * as DocumentPicker from 'expo-document-picker'; import { upload } from '../lib/upload'; export default function DocumentUploader() { const { uploadFiles, files, isUploading, errors } = upload.documentUpload(); const pickAndUpload = async () => { const result = await DocumentPicker.getDocumentAsync({ type: ['application/pdf', 'text/plain'], multiple: true, }); if (!result.canceled) { // Pass result.assets directly — no mapping needed await uploadFiles(result.assets); } }; return ( {isUploading ? 'Uploading...' : 'Pick Documents'} {errors.length > 0 && ( {errors[0]} )} {files.map((file) => ( {file.name} {file.status === 'success' ? 'Uploaded' : `${file.progress}%`} ))} ); } ``` ### react-native-image-picker `react-native-image-picker` types `uri` as optional on its `Asset` type. Filter for `uri` before passing: ```typescript import { launchImageLibrary } from 'react-native-image-picker'; import { useUploadRoute } from 'pushduck/react-native'; const { uploadFiles, isUploading } = useUploadRoute('imageUpload', { endpoint: 'https://your-api.com/api/s3-upload', }); const pickAndUpload = async () => { const result = await launchImageLibrary({ mediaType: 'photo', selectionLimit: 0 }); // uri is typed as optional — filter to narrow the type before passing const assets = result.assets?.filter( (a): a is typeof a & { uri: string } => !!a.uri ); if (assets?.length) await uploadFiles(assets); }; ``` *** ## Progress Tracking All the same progress state is available in React Native: ```typescript import { useUploadRoute, formatETA, formatUploadSpeed } from 'pushduck/react-native'; const { uploadFiles, files, // per-file state: name, status, progress, url, error isUploading, // true while any upload is in progress progress, // 0–100 overall across all files uploadSpeed, // bytes/second overall eta, // seconds remaining overall errors, // string[] of error messages reset, // clears all state } = useUploadRoute('imageUpload', { endpoint: 'https://your-api.com/api/s3-upload', onProgress: (pct) => console.log(`${pct}%`), onSuccess: (files) => console.log('Done:', files.map(f => f.url)), onError: (err) => console.error(err.message), }); // In your render: // {isUploading && Speed: {formatUploadSpeed(uploadSpeed ?? 0)} — ETA: {formatETA(eta ?? 0)}} ``` *** ## Authentication Pass a token via the `fetcher` option to attach headers to every request: ```typescript import { createUploadClient } from 'pushduck/react-native'; import type { AppRouter } from './upload-server'; import { getAuthToken } from './auth'; // your token source export const upload = createUploadClient({ endpoint: 'https://your-api.com/api/s3-upload', fetcher: async (url, init) => { const token = await getAuthToken(); return fetch(url, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${token}` }, }); }, }); ``` On the server, read the token from the request header in your middleware: ```typescript imageUpload: s3 .image() .maxFileSize('10MB') .middleware(async ({ req }) => { const token = req.headers.get('authorization')?.replace('Bearer ', ''); if (!token) throw new Error('Unauthorized'); const userId = await verifyToken(token); return { userId }; }), ``` *** ## Expo Router (Full-Stack) If you're using Expo Router with API routes, the server runs inside your Expo app. Expo Router API routes use standard `Request`/`Response`, so no adapter is needed. ### API Route ```typescript title="app/api/s3-upload/[...slug]+api.ts" import { uploadRouter } from '../../../lib/upload'; export async function GET(request: Request) { return uploadRouter.handlers(request); } export async function POST(request: Request) { return uploadRouter.handlers(request); } ``` ### Enable Server Output ```json title="app.json" { "expo": { "web": { "output": "server" }, "plugins": [["expo-router", { "origin": "https://your-domain.com" }]] } } ``` ### Client Setup for Expo Router When the server is inside the Expo app itself, the endpoint is still an absolute URL — use `EXPO_PUBLIC_API_URL` to configure it: ```typescript title="lib/upload.ts" import { createUploadClient } from 'pushduck/react-native'; import type { AppRouter } from './upload-server'; export const upload = createUploadClient({ endpoint: `${process.env.EXPO_PUBLIC_API_URL}/api/s3-upload`, }); ``` ```bash title=".env" EXPO_PUBLIC_API_URL=https://your-domain.com # Simulator/emulator local dev: http://localhost:8081 # Physical device local dev: use your machine's LAN IP (e.g. http://192.168.1.42:8081) # or run `npx expo start --tunnel` and use the tunnel URL ``` *** ## Environment Variables ```bash title=".env" # Server-side (API routes / backend) — no EXPO_PUBLIC_ prefix needed AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_ENDPOINT_URL=https://your-account.r2.cloudflarestorage.com S3_BUCKET_NAME=your-bucket R2_ACCOUNT_ID=your-cloudflare-account-id # Client-side — must use EXPO_PUBLIC_ prefix so Metro bundles them EXPO_PUBLIC_API_URL=https://your-domain.com ``` Server-only variables (without `EXPO_PUBLIC_`) are available in Expo Router API routes but **not** in the React Native client bundle. Use `EXPO_PUBLIC_API_URL` to pass the API base URL to the client. *** ## Requirements & Known Limitations **Minimum supported versions:** * Expo SDK 50+ / React Native 0.73+ recommended * expo-image-picker 15+ (SDK 50) for reliable `mimeType` field * React Native 0.72+ for the `File` global Older versions work for upload delivery but may store incorrect MIME types in S3 (e.g., `'image'` instead of `'image/jpeg'`) because `mimeType` was absent from expo-image-picker before SDK 50. **Known upstream bugs (not fixable in pushduck):** **Upload progress may be missing in Expo Go (SDK 49–51).** XHR upload progress events do not fire reliably in Expo Go on those SDK versions due to [a network debugging regression](https://github.com/expo/expo/issues/28269). Production builds (EAS Build) and development builds (expo-dev-client) are not affected. Uploads complete correctly — only progress tracking is missing. **`fetch('file://')` on Android is broken in React Native 0.82.0.** If you're on Expo SDK 53 with RN 0.82, uploads will fail on Android with a scheme error. This was fixed in RN 0.83.1 / Expo SDK 53.x patch. Update your Expo SDK to resolve it. *** ## Troubleshooting **Network request failed with a relative URL** You passed a relative endpoint. Fix it: ```typescript // ❌ fails in React Native useUploadRoute('imageUpload') // ✅ correct useUploadRoute('imageUpload', { endpoint: 'https://your-api.com/api/s3-upload' }) ``` **`Cannot read content:// URIs` error** You passed `copyToCacheDirectory: false` to `expo-document-picker`. Remove that option (or set it to `true`) so the picker copies the file to cache and returns a `file://` URI: ```typescript // ❌ returns content:// on Android — not supported const result = await DocumentPicker.getDocumentAsync({ copyToCacheDirectory: false }); // ✅ default behaviour — returns file:// URIs const result = await DocumentPicker.getDocumentAsync({ type: '*/*' }); ``` **Files uploaded with wrong MIME type / stored as `application/octet-stream`** This happens when your version of `expo-image-picker` doesn't populate the `mimeType` field (SDK \< 50). Upgrade to Expo SDK 50+ to resolve it. In the meantime the file is still uploaded and stored correctly — only the `Content-Type` metadata on the S3 object is wrong. **Upload never starts / no progress** Check that your picker returned actual assets — `result.canceled` might be `true`. If you're in Expo Go on SDK 49–51, progress won't show (see limitations above), but the upload is still happening. **Permission denied** Request media library permissions before calling the picker: ```typescript const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') return; ``` **CORS errors (Expo Router or separate backend)** React Native's native HTTP stack doesn't enforce browser CORS, so CORS errors only appear when running on **web** via Expo Router. Add CORS headers to your API route: ```typescript // Use a specific origin in production, not a wildcard. const allowedOrigin = process.env.APP_ORIGIN ?? 'https://your-domain.com'; export async function OPTIONS() { return new Response(null, { status: 200, headers: { 'Access-Control-Allow-Origin': allowedOrigin, 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }); } export async function POST(request: Request) { const response = await uploadRouter.handlers(request); response.headers.set('Access-Control-Allow-Origin', allowedOrigin); return response; } ``` **Metro cache issues** ```bash npx expo start --clear ``` # Express (/docs/integrations/express) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; ## Using pushduck with Express Express uses the traditional Node.js `req`/`res` API pattern. Pushduck provides a simple adapter that converts Web Standard handlers to Express middleware format. **Custom Request/Response API**: Express uses `req`/`res` objects instead of Web Standards, so pushduck provides the `toExpressHandler` adapter for straightforward integration. ## Quick Setup **Install dependencies** ```bash npm install pushduck ``` ```bash yarn add pushduck ``` ```bash pnpm add pushduck ``` ```bash bun add pushduck ``` **Configure upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create Express server with upload routes** ```typescript title="server.ts" import express from 'express'; import { uploadRouter } from './lib/upload'; import { toExpressHandler } from 'pushduck/adapters'; const app = express(); // Convert pushduck handlers to Express middleware app.all('/api/upload/*', toExpressHandler(uploadRouter.handlers)); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); }); ``` ## Basic Integration ### Simple Upload Route ```typescript title="server.ts" import express from 'express'; import cors from 'cors'; import { uploadRouter } from './lib/upload'; import { toExpressHandler } from 'pushduck/adapters'; const app = express(); // Middleware app.use(cors()); app.use(express.json()); // Upload routes using adapter app.all('/api/upload/*', toExpressHandler(uploadRouter.handlers)); // Health check app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString() }); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); }); ``` ### With Authentication Middleware ```typescript title="server.ts" import express from 'express'; import jwt from 'jsonwebtoken'; import { uploadRouter } from './lib/upload'; import { toExpressHandler } from 'pushduck/adapters'; const app = express(); app.use(express.json()); // Authentication middleware const authenticateToken = (req: express.Request, res: express.Response, next: express.NextFunction) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.sendStatus(401); } jwt.verify(token, process.env.JWT_SECRET!, (err, user) => { if (err) return res.sendStatus(403); req.user = user; next(); }); }; // Public upload route (no auth) app.all('/api/upload/public/*', toExpressHandler(uploadRouter.handlers)); // Private upload route (with auth) app.all('/api/upload/private/*', authenticateToken, toExpressHandler(uploadRouter.handlers)); app.listen(3000); ``` ## Advanced Configuration ### Upload Configuration with Express Context ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Profile pictures with authentication profilePicture: s3 .image() .maxFileSize("2MB") .maxFiles(1) .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ req }) => { // Extract user from JWT token in Authorization header const authHeader = req.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authentication required'); } const token = authHeader.substring(7); const user = await verifyJWT(token); return { userId: user.id, userRole: user.role, category: "profile" }; }), // Document uploads for authenticated users documents: s3 .file() .maxFileSize("10MB") .maxFiles(5) .accept([ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "text/plain" ]) .middleware(async ({ req }) => { const authHeader = req.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authentication required'); } const token = authHeader.substring(7); const user = await verifyJWT(token); return { userId: user.id, category: "documents" }; }), // Public uploads (no authentication) publicImages: s3 .image() .maxFileSize("1MB") .maxFiles(1) .accept(["image/jpeg", "image/png"]) // No middleware = public access }); async function verifyJWT(token: string) { // Your JWT verification logic const jwt = await import('jsonwebtoken'); return jwt.verify(token, process.env.JWT_SECRET!) as any; } export type AppUploadRouter = typeof uploadRouter; ``` ### Complete Express Application ```typescript title="server.ts" import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import { uploadRouter } from './lib/upload'; import { toExpressHandler } from 'pushduck/adapters'; const app = express(); // Security middleware app.use(helmet()); app.use(cors({ origin: process.env.NODE_ENV === 'production' ? ['https://your-domain.com'] : ['http://localhost:3000'], credentials: true })); // Rate limiting const uploadLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: 'Too many upload requests from this IP, please try again later.', standardHeaders: true, legacyHeaders: false, }); // Body parsing middleware app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); // Logging middleware app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); next(); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), version: process.env.npm_package_version || '1.0.0' }); }); // API info endpoint app.get('/api', (req, res) => { res.json({ name: 'Express Upload API', version: '1.0.0', endpoints: { health: '/health', upload: '/api/upload/*' }, uploadTypes: [ 'profilePicture - Single profile picture (2MB max)', 'documents - PDF, Word, text files (10MB max, 5 files)', 'publicImages - Public images (1MB max)' ] }); }); // Upload routes with rate limiting app.all('/api/upload/*', uploadLimiter, toExpressHandler(uploadRouter.handlers)); // 404 handler app.use('*', (req, res) => { res.status(404).json({ error: 'Not Found', message: `Route ${req.originalUrl} not found`, timestamp: new Date().toISOString() }); }); // Error handler app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('Express error:', err); res.status(500).json({ error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong', timestamp: new Date().toISOString() }); }); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Express server running on http://localhost:${port}`); console.log(`📁 Upload endpoint: http://localhost:${port}/api/upload`); }); ``` ## Project Structure ## Modular Route Organization ### Separate Upload Routes ```typescript title="routes/uploads.ts" import { Router } from 'express'; import { uploadRouter } from '../lib/upload'; import { toExpressHandler } from 'pushduck/adapters'; import { authenticateToken } from '../middleware/auth'; const router = Router(); // Public uploads router.all('/public/*', toExpressHandler(uploadRouter.handlers)); // Private uploads (requires authentication) router.all('/private/*', authenticateToken, toExpressHandler(uploadRouter.handlers)); export default router; ``` ```typescript title="middleware/auth.ts" import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; export const authenticateToken = (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Access token required' }); } jwt.verify(token, process.env.JWT_SECRET!, (err, user) => { if (err) { return res.status(403).json({ error: 'Invalid or expired token' }); } req.user = user; next(); }); }; ``` # Fastify (/docs/integrations/fastify) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Using pushduck with Fastify Fastify is a high-performance Node.js web framework that uses custom `request`/`reply` objects. Pushduck provides a simple adapter that converts Web Standard handlers to Fastify handler format. **Custom Request/Response API**: Fastify uses `request`/`reply` objects instead of Web Standards, so pushduck provides the `toFastifyHandler` adapter for straightforward integration. ## Quick Setup **Install dependencies** ```bash npm install pushduck ``` ```bash yarn add pushduck ``` ```bash pnpm add pushduck ``` ```bash bun add pushduck ``` **Configure upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create Fastify server with upload routes** ```typescript title="server.ts" import Fastify from 'fastify'; import { uploadRouter } from './lib/upload'; import { toFastifyHandler } from 'pushduck/adapters'; const fastify = Fastify({ logger: true }); // Convert pushduck handlers to Fastify handler fastify.all('/api/upload/*', toFastifyHandler(uploadRouter.handlers)); const start = async () => { try { await fastify.listen({ port: 3000 }); console.log('Fastify server running on http://localhost:3000'); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); ``` ## Basic Integration ### Simple Upload Route ```typescript title="server.ts" import Fastify from 'fastify'; import cors from '@fastify/cors'; import { uploadRouter } from './lib/upload'; import { toFastifyHandler } from 'pushduck/adapters'; const fastify = Fastify({ logger: { level: 'info', transport: { target: 'pino-pretty' } } }); // Register CORS await fastify.register(cors, { origin: ['http://localhost:3000', 'https://your-domain.com'] }); // Upload routes using adapter fastify.all('/api/upload/*', toFastifyHandler(uploadRouter.handlers)); // Health check fastify.get('/health', async (request, reply) => { return { status: 'healthy', timestamp: new Date().toISOString(), framework: 'Fastify' }; }); const start = async () => { try { await fastify.listen({ port: 3000, host: '0.0.0.0' }); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); ``` ### With Authentication Hook ```typescript title="server.ts" import Fastify from 'fastify'; import jwt from '@fastify/jwt'; import { uploadRouter } from './lib/upload'; import { toFastifyHandler } from 'pushduck/adapters'; const fastify = Fastify({ logger: true }); // Register JWT await fastify.register(jwt, { secret: process.env.JWT_SECRET! }); // Authentication hook fastify.addHook('preHandler', async (request, reply) => { // Only protect upload routes if (request.url.startsWith('/api/upload/private/')) { try { await request.jwtVerify(); } catch (err) { reply.send(err); } } }); // Public upload routes fastify.all('/api/upload/public/*', toFastifyHandler(uploadRouter.handlers)); // Private upload routes (protected by hook) fastify.all('/api/upload/private/*', toFastifyHandler(uploadRouter.handlers)); const start = async () => { try { await fastify.listen({ port: 3000 }); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); ``` ## Advanced Configuration ### Upload Configuration with Fastify Context ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Profile pictures with authentication profilePicture: s3 .image() .maxFileSize("2MB") .maxFiles(1) .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ req }) => { const authHeader = req.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authentication required'); } const token = authHeader.substring(7); const user = await verifyJWT(token); return { userId: user.id, userRole: user.role, category: "profile" }; }), // Document uploads for authenticated users documents: s3 .file() .maxFileSize("10MB") .maxFiles(5) .accept([ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "text/plain" ]) .middleware(async ({ req }) => { const authHeader = req.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authentication required'); } const token = authHeader.substring(7); const user = await verifyJWT(token); return { userId: user.id, category: "documents" }; }), // Public uploads (no authentication) publicImages: s3 .image() .maxFileSize("1MB") .maxFiles(1) .accept(["image/jpeg", "image/png"]) // No middleware = public access }); async function verifyJWT(token: string) { // Your JWT verification logic const jwt = await import('jsonwebtoken'); return jwt.verify(token, process.env.JWT_SECRET!) as any; } export type AppUploadRouter = typeof uploadRouter; ``` ### Complete Fastify Application ```typescript title="server.ts" import Fastify from 'fastify'; import cors from '@fastify/cors'; import helmet from '@fastify/helmet'; import rateLimit from '@fastify/rate-limit'; import { uploadRouter } from './lib/upload'; import { toFastifyHandler } from 'pushduck/adapters'; const fastify = Fastify({ logger: { level: process.env.NODE_ENV === 'production' ? 'warn' : 'info', transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty' } : undefined } }); // Security middleware await fastify.register(helmet, { contentSecurityPolicy: false }); // CORS configuration await fastify.register(cors, { origin: process.env.NODE_ENV === 'production' ? ['https://your-domain.com'] : true, credentials: true }); // Rate limiting await fastify.register(rateLimit, { max: 100, timeWindow: '15 minutes', errorResponseBuilder: (request, context) => ({ error: 'Rate limit exceeded', message: `Too many requests from ${request.ip}. Try again later.`, retryAfter: Math.round(context.ttl / 1000) }) }); // Request logging fastify.addHook('onRequest', async (request, reply) => { request.log.info({ url: request.url, method: request.method }, 'incoming request'); }); // Health check endpoint fastify.get('/health', async (request, reply) => { return { status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: process.memoryUsage(), version: process.env.npm_package_version || '1.0.0', framework: 'Fastify' }; }); // API info endpoint fastify.get('/api', async (request, reply) => { return { name: 'Fastify Upload API', version: '1.0.0', endpoints: { health: '/health', upload: '/api/upload/*' }, uploadTypes: [ 'profilePicture - Single profile picture (2MB max)', 'documents - PDF, Word, text files (10MB max, 5 files)', 'publicImages - Public images (1MB max)' ] }; }); // Upload routes with rate limiting fastify.register(async function (fastify) { await fastify.register(rateLimit, { max: 50, timeWindow: '15 minutes' }); fastify.all('/api/upload/*', toFastifyHandler(uploadRouter.handlers)); }); // 404 handler fastify.setNotFoundHandler(async (request, reply) => { reply.status(404).send({ error: 'Not Found', message: `Route ${request.method} ${request.url} not found`, timestamp: new Date().toISOString() }); }); // Error handler fastify.setErrorHandler(async (error, request, reply) => { request.log.error(error, 'Fastify error'); reply.status(500).send({ error: 'Internal Server Error', message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong', timestamp: new Date().toISOString() }); }); // Graceful shutdown const gracefulShutdown = () => { fastify.log.info('Shutting down gracefully...'); fastify.close().then(() => { fastify.log.info('Server closed'); process.exit(0); }).catch((err) => { fastify.log.error(err, 'Error during shutdown'); process.exit(1); }); }; process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown); const start = async () => { try { const port = Number(process.env.PORT) || 3000; const host = process.env.HOST || '0.0.0.0'; await fastify.listen({ port, host }); fastify.log.info(`Fastify server running on http://${host}:${port}`); fastify.log.info(`📁 Upload endpoint: http://${host}:${port}/api/upload`); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); ``` ## Plugin-Based Architecture ### Upload Plugin ```typescript title="plugins/upload.ts" import { FastifyPluginAsync } from 'fastify'; import { uploadRouter } from '../lib/upload'; import { toFastifyHandler } from 'pushduck/adapters'; const uploadPlugin: FastifyPluginAsync = async (fastify) => { // Upload routes fastify.all('/upload/*', toFastifyHandler(uploadRouter.handlers)); // Upload status endpoint fastify.get('/upload-status', async (request, reply) => { return { status: 'ready', supportedTypes: ['images', 'documents', 'publicImages'], maxSizes: { profilePicture: '2MB', documents: '10MB', publicImages: '1MB' } }; }); }; export default uploadPlugin; ``` ### Main Server with Plugins ```typescript title="server.ts" import Fastify from 'fastify'; import uploadPlugin from './plugins/upload'; const fastify = Fastify({ logger: true }); // Register upload plugin await fastify.register(uploadPlugin, { prefix: '/api' }); const start = async () => { try { await fastify.listen({ port: 3000 }); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); ``` ## Client Usage The client-side integration is identical regardless of your backend framework: ```typescript title="client/upload-client.ts" import { createUploadClient } from 'pushduck/client'; import type { AppUploadRouter } from '../lib/upload'; export const upload = createUploadClient({ endpoint: 'http://localhost:3000/api/upload', headers: { 'Authorization': `Bearer ${getAuthToken()}` } }); function getAuthToken(): string { return localStorage.getItem('auth-token') || ''; } ``` ```typescript title="client/upload-form.tsx" import { upload } from './upload-client'; export function DocumentUploader() { const { uploadFiles, files, isUploading, error } = upload.documents(); const handleFileSelect = (e: React.ChangeEvent) => { const selectedFiles = Array.from(e.target.files || []); uploadFiles(selectedFiles); }; return (
{error && (
Error: {error.message}
)} {files.map((file) => (
{file.name} {file.status === 'success' && ( Download )}
))}
); } ``` ## Deployment ### Docker Deployment ```dockerfile title="Dockerfile" FROM node:18-alpine WORKDIR /app # Copy package files COPY package*.json ./ RUN npm ci --only=production # Copy source code COPY . . # Build TypeScript RUN npm run build EXPOSE 3000 CMD ["npm", "start"] ``` ### Package Configuration ```json title="package.json" { "name": "fastify-upload-api", "version": "1.0.0", "scripts": { "dev": "tsx watch src/server.ts", "build": "tsc", "start": "node dist/server.js" }, "dependencies": { "fastify": "^4.24.0", "pushduck": "latest", "@fastify/cors": "^8.4.0", "@fastify/helmet": "^11.1.0", "@fastify/rate-limit": "^8.0.0", "@fastify/jwt": "^7.2.0" }, "devDependencies": { "@types/node": "^20.0.0", "tsx": "^3.12.7", "typescript": "^5.0.0", "pino-pretty": "^10.2.0" } } ``` ### Environment Variables ```bash title=".env" # Server Configuration PORT=3000 HOST=0.0.0.0 NODE_ENV=development JWT_SECRET=your-super-secret-jwt-key # Cloudflare R2 Configuration AWS_ACCESS_KEY_ID=your_r2_access_key AWS_SECRET_ACCESS_KEY=your_r2_secret_key AWS_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com S3_BUCKET_NAME=your-bucket-name R2_ACCOUNT_ID=your-account-id ``` ## Performance Benefits Fastify is one of the fastest Node.js frameworks, perfect for high-throughput upload APIs. Leverage Fastify's extensive plugin ecosystem alongside pushduck's upload capabilities. Excellent TypeScript support with full type safety for both Fastify and pushduck. Built-in schema validation, logging, and error handling for production deployments. *** **Fastify + Pushduck**: High-performance file uploads with Fastify's speed and pushduck's universal design, connected through a simple adapter. # Fresh (/docs/integrations/fresh) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; **🚧 Client-Side In Development**: Fresh server-side integration is fully functional with Web Standards APIs. However, Fresh-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now. ## Using pushduck with Fresh Fresh is a modern web framework for Deno that uses islands architecture for optimal performance. It uses Web Standards APIs and provides server-side rendering with minimal client-side JavaScript. Since Fresh uses standard `Request`/`Response` objects, pushduck handlers work directly without any adapters! **Web Standards Native**: Fresh API routes use Web Standard `Request`/`Response` objects, making pushduck integration straightforward with zero overhead. ## Quick Setup **Install Fresh and pushduck** ```bash # Create a new Fresh project deno run -A -r https://fresh.deno.dev my-app cd my-app # Add pushduck to import_map.json ``` ```json title="import_map.json" { "imports": { "$fresh/": "https://deno.land/x/fresh@1.6.1/", "preact": "https://esm.sh/preact@10.19.2", "preact/": "https://esm.sh/preact@10.19.2/", "pushduck/server": "https://esm.sh/pushduck@latest/server", "pushduck/client": "https://esm.sh/pushduck@latest/client" } } ``` ```bash # Create a new Fresh project deno run -A -r https://fresh.deno.dev my-app cd my-app # Install pushduck via npm (requires Node.js compatibility) npm install pushduck ``` ```bash # Create a new Fresh project deno run -A -r https://fresh.deno.dev my-app cd my-app # Install pushduck via yarn (requires Node.js compatibility) yarn add pushduck ``` ```bash # Create a new Fresh project deno run -A -r https://fresh.deno.dev my-app cd my-app # Install pushduck via pnpm (requires Node.js compatibility) pnpm add pushduck ``` ```bash # Create a new Fresh project deno run -A -r https://fresh.deno.dev my-app cd my-app # Install pushduck via bun (requires Node.js compatibility) bun add pushduck ``` **Configure upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: Deno.env.get("AWS_ACCESS_KEY_ID")!, secretAccessKey: Deno.env.get("AWS_SECRET_ACCESS_KEY")!, region: 'auto', endpoint: Deno.env.get("AWS_ENDPOINT_URL")!, bucket: Deno.env.get("S3_BUCKET_NAME")!, accountId: Deno.env.get("R2_ACCOUNT_ID")!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create API route** ```typescript title="routes/api/upload/[...path].ts" import { Handlers } from "$fresh/server.ts"; import { uploadRouter } from "../../../lib/upload.ts"; // Direct usage - no adapter needed! export const handler: Handlers = { async GET(req) { return uploadRouter.handlers(req); }, async POST(req) { return uploadRouter.handlers(req); }, }; ``` ## Basic Integration ### Simple Upload Route ```typescript title="routes/api/upload/[...path].ts" import { Handlers } from "$fresh/server.ts"; import { uploadRouter } from "../../../lib/upload.ts"; // Method 1: Combined handler (recommended) export const handler: Handlers = { async GET(req) { return uploadRouter.handlers(req); }, async POST(req) { return uploadRouter.handlers(req); }, }; // Method 2: Universal handler export const handler: Handlers = { async GET(req) { return uploadRouter.handlers(req); }, async POST(req) { return uploadRouter.handlers(req); }, async OPTIONS(req) { return new Response(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', }, }); }, }; ``` ### With Middleware ```typescript title="routes/_middleware.ts" import { MiddlewareHandlerContext } from "$fresh/server.ts"; export async function handler( req: Request, ctx: MiddlewareHandlerContext, ) { // Add CORS headers for upload routes if (ctx.destination === "route" && req.url.includes("/api/upload")) { const response = await ctx.next(); response.headers.set("Access-Control-Allow-Origin", "*"); response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); response.headers.set("Access-Control-Allow-Headers", "Content-Type"); return response; } return ctx.next(); } ``` ## Advanced Configuration ### Authentication with Fresh ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; import { getCookies } from "https://deno.land/std@0.208.0/http/cookie.ts"; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: Deno.env.get("AWS_ACCESS_KEY_ID")!, secretAccessKey: Deno.env.get("AWS_SECRET_ACCESS_KEY")!, region: 'auto', endpoint: Deno.env.get("AWS_ENDPOINT_URL")!, bucket: Deno.env.get("S3_BUCKET_NAME")!, accountId: Deno.env.get("R2_ACCOUNT_ID")!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with cookie-based authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const cookies = getCookies(req.headers); const sessionId = cookies.sessionId; if (!sessionId) { throw new Error('Authentication required'); } const user = await getUserFromSession(sessionId); if (!user) { throw new Error('Invalid session'); } return { userId: user.id, username: user.username, }; }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; // Helper function async function getUserFromSession(sessionId: string) { // Implement your session validation logic // This could connect to a database, Deno KV, etc. return { id: 'user-123', username: 'demo-user' }; } ``` ## Client-Side Usage ### Upload Island Component ```tsx title="islands/FileUpload.tsx" import { useUpload } from "pushduck/client"; import type { AppUploadRouter } from "../lib/upload.ts"; const { UploadButton, UploadDropzone } = useUpload({ endpoint: "/api/upload", }); export default function FileUpload() { function handleUploadComplete(files: any[]) { console.log("Files uploaded:", files); alert("Upload completed!"); } function handleUploadError(error: Error) { console.error("Upload error:", error); alert(`Upload failed: ${error.message}`); } return (

Image Upload

Document Upload

); } ``` ### Using in Pages ```tsx title="routes/index.tsx" import { Head } from "$fresh/runtime.ts"; import FileUpload from "../islands/FileUpload.tsx"; export default function Home() { return ( <> File Upload Demo

File Upload Demo

); } ``` ## File Management ### Server-Side File API ```typescript title="routes/api/files.ts" import { Handlers } from "$fresh/server.ts"; export const handler: Handlers = { async GET(req) { const url = new URL(req.url); const userId = url.searchParams.get('userId'); if (!userId) { return new Response(JSON.stringify({ error: 'User ID required' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); } // Fetch files from database/Deno KV const files = await getFilesForUser(userId); return new Response(JSON.stringify({ files: files.map(file => ({ id: file.id, name: file.name, url: file.url, size: file.size, uploadedAt: file.createdAt, })), }), { headers: { 'Content-Type': 'application/json' } }); }, }; async function getFilesForUser(userId: string) { // Example using Deno KV const kv = await Deno.openKv(); const files = []; for await (const entry of kv.list({ prefix: ["files", userId] })) { files.push(entry.value); } return files; } ``` ### File Management Page ```tsx title="routes/files.tsx" import { Head } from "$fresh/runtime.ts"; import { Handlers, PageProps } from "$fresh/server.ts"; import FileUpload from "../islands/FileUpload.tsx"; interface FileData { id: string; name: string; url: string; size: number; uploadedAt: string; } interface PageData { files: FileData[]; } export const handler: Handlers = { async GET(req, ctx) { // Fetch files for current user const files = await getFilesForUser("current-user"); return ctx.render({ files }); }, }; export default function FilesPage({ data }: PageProps) { function formatFileSize(bytes: number): string { const sizes = ['Bytes', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } return ( <> My Files

My Files

Uploaded Files

{data.files.length === 0 ? (

No files uploaded yet.

) : (
{data.files.map((file) => (

{file.name}

{formatFileSize(file.size)}

{new Date(file.uploadedAt).toLocaleDateString()}

View File
))}
)}
); } async function getFilesForUser(userId: string) { // Implementation depends on your storage solution return []; } ``` ## Deployment Options ```bash # Deploy to Deno Deploy deno task build deployctl deploy --project=my-app --include=. --exclude=node_modules ``` ```json title="deno.json" { "tasks": { "build": "deno run -A dev.ts build", "preview": "deno run -A main.ts", "start": "deno run -A --watch=static/,routes/ dev.ts", "deploy": "deployctl deploy --project=my-app --include=. --exclude=node_modules" } } ``` ```dockerfile title="Dockerfile" FROM denoland/deno:1.38.0 WORKDIR /app # Copy dependency files COPY deno.json deno.lock import_map.json ./ # Cache dependencies RUN deno cache --import-map=import_map.json main.ts # Copy source code COPY . . # Build the application RUN deno task build EXPOSE 8000 CMD ["deno", "run", "-A", "main.ts"] ``` ```bash # Install Deno curl -fsSL https://deno.land/install.sh | sh # Clone and run your app git clone cd deno task start ``` ```systemd title="/etc/systemd/system/fresh-app.service" [Unit] Description=Fresh App After=network.target [Service] Type=simple User=deno WorkingDirectory=/opt/fresh-app ExecStart=/home/deno/.deno/bin/deno run -A main.ts Restart=always [Install] WantedBy=multi-user.target ``` ## Environment Variables ```bash title=".env" # AWS Configuration AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_S3_BUCKET=your-bucket-name # Fresh PORT=8000 ``` ## Performance Benefits ## Real-Time Upload Progress ```tsx title="islands/AdvancedUpload.tsx" import { useState } from "preact/hooks"; export default function AdvancedUpload() { const [uploadProgress, setUploadProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); async function handleFileUpload(event: Event) { const target = event.target as HTMLInputElement; const files = target.files; if (!files || files.length === 0) return; setIsUploading(true); setUploadProgress(0); try { // Simulate upload progress for (let i = 0; i <= 100; i += 10) { setUploadProgress(i); await new Promise(resolve => setTimeout(resolve, 100)); } alert('Upload completed!'); } catch (error) { console.error('Upload failed:', error); alert('Upload failed!'); } finally { setIsUploading(false); setUploadProgress(0); } } return (
{isUploading && (

{uploadProgress}% uploaded

)}
); } ``` ## Deno KV Integration ```typescript title="lib/storage.ts" // Example using Deno KV for file metadata storage export class FileStorage { private kv: Deno.Kv; constructor() { this.kv = await Deno.openKv(); } async saveFileMetadata(userId: string, file: { id: string; name: string; url: string; size: number; type: string; }) { const key = ["files", userId, file.id]; await this.kv.set(key, { ...file, createdAt: new Date().toISOString(), }); } async getFilesForUser(userId: string) { const files = []; for await (const entry of this.kv.list({ prefix: ["files", userId] })) { files.push(entry.value); } return files; } async deleteFile(userId: string, fileId: string) { const key = ["files", userId, fileId]; await this.kv.delete(key); } } export const fileStorage = new FileStorage(); ``` ## Troubleshooting **Common Issues** 1. **Route not found**: Ensure your route is `routes/api/upload/[...path].ts` 2. **Import errors**: Check your `import_map.json` configuration 3. **Permissions**: Deno requires explicit permissions (`-A` flag for all permissions) 4. **Environment variables**: Use `Deno.env.get()` instead of `process.env` ### Debug Mode Enable debug logging: ```typescript title="lib/upload.ts" export const uploadRouter = createS3Router({ // ... routes }).middleware(async ({ req, file }) => { if (Deno.env.get("DENO_ENV") === "development") { console.log("Upload request:", req.url); console.log("File:", file.name, file.size); } return {}; }); ``` ### Fresh Configuration ```typescript title="fresh.config.ts" import { defineConfig } from "$fresh/server.ts"; export default defineConfig({ plugins: [], // Enable static file serving staticDir: "./static", // Custom build options build: { target: ["chrome99", "firefox99", "safari15"], }, }); ``` Fresh provides an excellent foundation for building modern web applications with Deno and pushduck, combining the power of islands architecture with Web Standards APIs and Deno's secure runtime environment. # Hono (/docs/integrations/hono) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; ## Using pushduck with Hono Hono is a fast, lightweight web framework built on Web Standards. Since Hono uses `Request` and `Response` objects natively, pushduck handlers work directly without any adapters! **Web Standards Native**: Hono exposes `c.req.raw` as a Web Standard `Request` object, making pushduck integration straightforward with zero overhead. ## Quick Setup **Install dependencies** ```bash npm install pushduck ``` ```bash yarn add pushduck ``` ```bash pnpm add pushduck ``` ```bash bun add pushduck ``` **Configure upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create Hono app with upload routes** ```typescript title="app.ts" import { Hono } from 'hono'; import { uploadRouter } from './lib/upload'; const app = new Hono(); // Direct usage - no adapter needed! app.all('/api/upload/*', (c) => { return uploadRouter.handlers(c.req.raw); }); export default app; ``` ## Basic Integration ### Simple Upload Route ```typescript title="app.ts" import { Hono } from 'hono'; import { uploadRouter } from './lib/upload'; const app = new Hono(); // Method 1: Combined handler (recommended) app.all('/api/upload/*', (c) => { return uploadRouter.handlers(c.req.raw); }); // Method 2: Separate handlers (if you need method-specific logic) app.get('/api/upload/*', (c) => uploadRouter.handlers.GET(c.req.raw)); app.post('/api/upload/*', (c) => uploadRouter.handlers.POST(c.req.raw)); export default app; ``` ### With Middleware ```typescript title="app.ts" import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; import { uploadRouter } from './lib/upload'; const app = new Hono(); // Global middleware app.use('*', logger()); app.use('*', cors({ origin: ['http://localhost:3000', 'https://your-domain.com'], allowMethods: ['GET', 'POST'], allowHeaders: ['Content-Type'], })); // Upload routes app.all('/api/upload/*', (c) => uploadRouter.handlers(c.req.raw)); // Health check app.get('/health', (c) => c.json({ status: 'ok' })); export default app; ``` ## Advanced Configuration ### Authentication with Hono ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; import { verify } from 'hono/jwt'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with JWT authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const authHeader = req.headers.get('authorization'); if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authorization required'); } const token = authHeader.substring(7); try { const payload = await verify(token, process.env.JWT_SECRET!); return { userId: payload.sub as string, userRole: payload.role as string }; } catch (error) { throw new Error('Invalid token'); } }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; ``` ## Deployment Options ```typescript title="src/index.ts" import { Hono } from 'hono'; import { uploadRouter } from './lib/upload'; const app = new Hono(); app.all('/api/upload/*', (c) => uploadRouter.handlers(c.req.raw)); export default app; ``` ```toml title="wrangler.toml" name = "my-upload-api" main = "src/index.ts" compatibility_date = "2023-12-01" [env.production] vars = { NODE_ENV = "production" } ``` ```bash # Deploy to Cloudflare Workers npx wrangler deploy ``` ```typescript title="server.ts" import { Hono } from 'hono'; import { uploadRouter } from './lib/upload'; const app = new Hono(); app.all('/api/upload/*', (c) => uploadRouter.handlers(c.req.raw)); export default { port: 3000, fetch: app.fetch, }; ``` ```bash # Run with Bun bun run server.ts ``` ```typescript title="server.ts" import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { uploadRouter } from './lib/upload'; const app = new Hono(); app.all('/api/upload/*', (c) => uploadRouter.handlers(c.req.raw)); const port = 3000; console.log(`Server is running on port ${port}`); serve({ fetch: app.fetch, port }); ``` ```bash # Run with Node.js npm run dev ``` ```typescript title="server.ts" import { Hono } from 'hono'; import { uploadRouter } from './lib/upload.ts'; const app = new Hono(); app.all('/api/upload/*', (c) => uploadRouter.handlers(c.req.raw)); Deno.serve(app.fetch); ``` ```bash # Run with Deno deno run --allow-net --allow-env server.ts ``` ## Performance Benefits No adapter layer means zero performance overhead - pushduck handlers run directly in Hono. Hono is one of the fastest web frameworks, perfect for high-performance upload APIs. Works on Cloudflare Workers, Bun, Node.js, and Deno with the same code. Hono + pushduck creates incredibly lightweight upload services. *** **Perfect Match**: Hono's Web Standards foundation and pushduck's universal design create a fast and lightweight file upload solution that works everywhere. # Framework Integrations (/docs/integrations) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; ## Supported Frameworks Pushduck provides **universal file upload handlers** that work with any web framework through a single, consistent API. Write your upload logic once and deploy it anywhere! **Universal Design**: Pushduck uses Web Standards (Request/Response) at its core, making it compatible with both Web Standards frameworks and those with custom request/response APIs without framework-specific code. ## Universal API All frameworks use the same core API: ```typescript import { createS3Router, s3 } from 'pushduck/server'; const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB"), videoUpload: s3.file().maxFileSize("100MB").accept(["video/*"]) }); // Universal handlers - work with ANY framework export const { GET, POST } = uploadRouter.handlers; ``` ## Framework Categories Pushduck supports frameworks in two categories: **No adapter needed!** Use `uploadRouter.handlers` directly. * Hono * Elysia * Bun Runtime * TanStack Start * SolidJS Start **Simple adapters provided** for straightforward integration. * Next.js (App & Pages Router) * Express * Fastify ## Quick Start by Framework ```typescript // Works with: Hono, Elysia, Bun, TanStack Start, SolidJS Start import { uploadRouter } from '@/lib/upload'; // Direct usage - no adapter needed! app.all('/api/upload/*', (ctx) => { return uploadRouter.handlers(ctx.request); // or c.req.raw }); ``` ```typescript // app/api/upload/route.ts import { uploadRouter } from '@/lib/upload'; // Direct usage (recommended) export const { GET, POST } = uploadRouter.handlers; // Or with explicit adapter for extra type safety import { toNextJsHandler } from 'pushduck/adapters'; export const { GET, POST } = toNextJsHandler(uploadRouter.handlers); ``` ```typescript import express from 'express'; import { uploadRouter } from '@/lib/upload'; import { toExpressHandler } from 'pushduck/adapters'; const app = express(); app.all("/api/upload/*", toExpressHandler(uploadRouter.handlers)); ``` ```typescript import Fastify from 'fastify'; import { uploadRouter } from '@/lib/upload'; import { toFastifyHandler } from 'pushduck/adapters'; const fastify = Fastify(); fastify.all('/api/upload/*', toFastifyHandler(uploadRouter.handlers)); ``` ## Why Universal Handlers Work **Web Standards Foundation** Pushduck is built on Web Standards (`Request` and `Response` objects) that are supported by all modern JavaScript runtimes. ```typescript // Core handler signature type Handler = (request: Request) => Promise ``` **Framework Compatibility** Modern frameworks expose Web Standard objects directly: * **Hono**: `c.req.raw` is a Web `Request` * **Elysia**: `context.request` is a Web `Request` * **Bun**: Native Web `Request` support * **TanStack Start**: `{ request }` is a Web `Request` * **SolidJS Start**: `event.request` is a Web `Request` **Framework Adapters** For frameworks with custom request/response APIs, simple adapters convert between formats: ```typescript // Express adapter example export function toExpressHandler(handlers: UniversalHandlers) { return async (req: Request, res: Response, next: NextFunction) => { const webRequest = convertExpressToWebRequest(req); const webResponse = await handlers[req.method](webRequest); convertWebResponseToExpress(webResponse, res); }; } ``` ## Configuration (Same for All Frameworks) Your upload configuration is identical across all frameworks: ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Image uploads with validation imageUpload: s3 .image() .maxFileSize("5MB") .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ req }) => { const userId = await getUserId(req); return { userId, category: "images" }; }), // Document uploads documentUpload: s3 .file() .maxFileSize("10MB") .accept(["application/pdf", "text/plain"]) .middleware(async ({ req }) => { const userId = await getUserId(req); return { userId, category: "documents" }; }), // Video uploads videoUpload: s3 .file() .maxFileSize("100MB") .accept(["video/mp4", "video/quicktime"]) .middleware(async ({ req }) => { const userId = await getUserId(req); return { userId, category: "videos" }; }) }); export type AppUploadRouter = typeof uploadRouter; ``` ## Client Usage (Framework Independent) The client-side code is identical regardless of your backend framework: ```typescript title="lib/upload-client.ts" import { createUploadClient } from 'pushduck/client'; import type { AppUploadRouter } from './upload'; export const upload = createUploadClient({ endpoint: '/api/upload' }); ``` ```typescript title="components/upload-form.tsx" import { upload } from '@/lib/upload-client'; export function UploadForm() { // Property-based access with full type safety const { uploadFiles, files, isUploading } = upload.imageUpload(); const handleUpload = async (selectedFiles: File[]) => { await uploadFiles(selectedFiles); }; return (
handleUpload(Array.from(e.target.files || []))} /> {files.map(file => (
{file.name} {file.url && View}
))}
); } ``` ## Benefits of Universal Design Migrate from Express to Hono or Next.js to Bun without changing your upload implementation. Web Standards native frameworks get direct handler access with no adapter overhead. Master pushduck once and use it with any framework in your toolkit. As more frameworks adopt Web Standards, they automatically work with pushduck. ## Next Steps Choose your framework integration guide: Complete guide for Next.js App Router and Pages Router Fast, lightweight, built on Web Standards TypeScript-first framework with Bun Classic Node.js framework integration *** **Universal by Design**: Write once, run anywhere. Pushduck's universal handlers make file uploads work across the entire JavaScript ecosystem. # Next.js (/docs/integrations/nextjs) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; ## Next.js Integration Pushduck provides straightforward integration with both Next.js App Router and Pages Router through universal handlers that work with Next.js's Web Standards-based API. **Next.js 13+**: App Router uses Web Standards (Request/Response), so pushduck handlers work directly. Pages Router requires a simple adapter for the legacy req/res API. ## Quick Setup **Install pushduck** npm pnpm yarn bun ```bash npm install pushduck ``` ```bash pnpm add pushduck ``` ```bash yarn add pushduck ``` ```bash bun add pushduck ``` **Configure your upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ // [!code highlight] accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, // [!code highlight] }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), // [!code highlight] documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create API route** App Router Pages Router ```typescript title="app/api/upload/route.ts" import { uploadRouter } from '@/lib/upload'; // Direct usage (recommended) export const { GET, POST } = uploadRouter.handlers; ``` ```typescript title="pages/api/upload/[...path].ts" import { uploadRouter } from '@/lib/upload'; import { toNextJsPagesHandler } from 'pushduck/server'; export default toNextJsPagesHandler(uploadRouter.handlers); ``` ## App Router Integration Next.js App Router uses Web Standards, making integration straightforward: ### Basic API Route ```typescript title="app/api/upload/route.ts" import { uploadRouter } from '@/lib/upload'; // Direct usage - works because Next.js App Router uses Web Standards export const { GET, POST } = uploadRouter.handlers; ``` ### With Type Safety Adapter For extra type safety and better IDE support: ```typescript title="app/api/upload/route.ts" import { uploadRouter } from '@/lib/upload'; import { toNextJsHandler } from 'pushduck/adapters'; // Explicit adapter for enhanced type safety export const { GET, POST } = toNextJsHandler(uploadRouter.handlers); ``` ### Advanced Configuration ```typescript title="app/api/upload/route.ts" import { createUploadConfig } from 'pushduck/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/lib/auth'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); const uploadRouter = createS3Router({ // Profile pictures with authentication profilePicture: s3 .image() .maxFileSize("2MB") .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ req }) => { const session = await getServerSession(authOptions); if (!session?.user?.id) { throw new Error("Authentication required"); } return { userId: session.user.id, category: "profile" }; }), // Document uploads for authenticated users documents: s3 .file() .maxFileSize("10MB") .accept(["application/pdf", "text/plain", "application/msword"]) .middleware(async ({ req }) => { const session = await getServerSession(authOptions); if (!session?.user?.id) { throw new Error("Authentication required"); } return { userId: session.user.id, category: "documents" }; }), // Public image uploads (no auth required) publicImages: s3 .image() .maxFileSize("5MB") .accept(["image/jpeg", "image/png", "image/webp"]) // No middleware = publicly accessible }); export type AppUploadRouter = typeof uploadRouter; export const { GET, POST } = uploadRouter.handlers; ``` ## Pages Router Integration Pages Router uses the legacy req/res API, so we provide a simple adapter: ### Basic API Route ```typescript title="pages/api/upload/[...path].ts" import { uploadRouter } from '@/lib/upload'; import { toNextJsPagesHandler } from 'pushduck/adapters'; export default toNextJsPagesHandler(uploadRouter.handlers); ``` ### With Authentication ```typescript title="pages/api/upload/[...path].ts" import { createUploadConfig } from 'pushduck/server'; import { toNextJsPagesHandler } from 'pushduck/adapters'; import { getSession } from 'next-auth/react'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ // ... your config }) .build(); const uploadRouter = createS3Router({ imageUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { // Convert Web Request to get session const session = await getSession({ req: req as any }); if (!session?.user?.id) { throw new Error("Authentication required"); } return { userId: session.user.id }; }) }); export default toNextJsPagesHandler(uploadRouter.handlers); ``` ## Client-Side Usage The client-side code is identical for both App Router and Pages Router: ### Setup Upload Client ```typescript title="lib/upload-client.ts" import { createUploadClient } from 'pushduck/client'; import type { AppUploadRouter } from './upload'; export const upload = createUploadClient({ endpoint: '/api/upload' }); ``` ### React Component ```typescript title="components/upload-form.tsx" 'use client'; // App Router // or just regular component for Pages Router import { upload } from '@/lib/upload-client'; import { useState } from 'react'; export function UploadForm() { const { uploadFiles, files, isUploading, error } = upload.imageUpload(); const handleFileSelect = (e: React.ChangeEvent) => { const selectedFiles = Array.from(e.target.files || []); uploadFiles(selectedFiles); }; return (
{error && (
Error: {error.message}
)} {files.length > 0 && (
{files.map((file) => (

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB

{file.status === 'success' ? 'Complete' : `${file.progress}%`}

{file.status === 'success' && file.url && ( View )}
))}
)}
); } ``` ## Project Structure Here's a recommended project structure for Next.js with pushduck: ## Complete Example ### Upload Configuration ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; import { getServerSession } from 'next-auth'; import { authOptions } from './auth'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { const timestamp = Date.now(); const randomId = Math.random().toString(36).substring(2, 8); return `${metadata.userId}/${timestamp}/${randomId}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Profile pictures - single image, authenticated profilePicture: s3 .image() .maxFileSize("2MB") .maxFiles(1) .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ req }) => { const session = await getServerSession(authOptions); if (!session?.user?.id) throw new Error("Authentication required"); return { userId: session.user.id, type: "profile" }; }), // Gallery images - multiple images, authenticated gallery: s3 .image() .maxFileSize("5MB") .maxFiles(10) .accept(["image/jpeg", "image/png", "image/webp"]) .middleware(async ({ req }) => { const session = await getServerSession(authOptions); if (!session?.user?.id) throw new Error("Authentication required"); return { userId: session.user.id, type: "gallery" }; }), // Documents - various file types, authenticated documents: s3 .file() .maxFileSize("10MB") .maxFiles(5) .accept([ "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "text/plain" ]) .middleware(async ({ req }) => { const session = await getServerSession(authOptions); if (!session?.user?.id) throw new Error("Authentication required"); return { userId: session.user.id, type: "documents" }; }), // Public uploads - no authentication required public: s3 .image() .maxFileSize("1MB") .maxFiles(1) .accept(["image/jpeg", "image/png"]) // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; ``` ### API Route (App Router) ```typescript title="app/api/upload/route.ts" import { uploadRouter } from '@/lib/upload'; export const { GET, POST } = uploadRouter.handlers; ``` ### Upload Page ```typescript title="app/upload/page.tsx" 'use client'; import { upload } from '@/lib/upload-client'; import { useState } from 'react'; export default function UploadPage() { const [activeTab, setActiveTab] = useState<'profile' | 'gallery' | 'documents'>('profile'); const profileUpload = upload.profilePicture(); const galleryUpload = upload.gallery(); const documentsUpload = upload.documents(); const currentUpload = { profile: profileUpload, gallery: galleryUpload, documents: documentsUpload }[activeTab]; return (

File Upload Demo

{/* Tab Navigation */}
{[ { key: 'profile', label: 'Profile Picture', icon: '👤' }, { key: 'gallery', label: 'Gallery', icon: '🖼️' }, { key: 'documents', label: 'Documents', icon: '📄' } ].map(tab => ( ))}
{/* Upload Interface */}
{ const files = Array.from(e.target.files || []); currentUpload.uploadFiles(files); }} disabled={currentUpload.isUploading} className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" /> {/* File List */} {currentUpload.files.length > 0 && (
{currentUpload.files.map((file) => (

{file.name}

{(file.size / 1024 / 1024).toFixed(2)} MB

{file.status === 'success' && '✅'} {file.status === 'error' && '❌'} {file.status === 'uploading' && '⏳'} {file.status === 'pending' && '⏸️'}
{file.status === 'success' && file.url && ( View )}
))}
)}
); } ``` ## Environment Variables ```bash title=".env.local" # Cloudflare R2 Configuration AWS_ACCESS_KEY_ID=your_r2_access_key AWS_SECRET_ACCESS_KEY=your_r2_secret_key AWS_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com S3_BUCKET_NAME=your-bucket-name R2_ACCOUNT_ID=your-account-id # Next.js Configuration NEXTAUTH_SECRET=your-nextauth-secret NEXTAUTH_URL=http://localhost:3000 ``` ## Deployment Considerations * Environment variables configured in dashboard * Edge Runtime compatible * Automatic HTTPS * Configure environment variables * Works with Netlify Functions * CDN integration available * Complete Next.js compatibility * Environment variable management * Automatic deployments *** **Next.js Ready**: Pushduck works with both Next.js App Router and Pages Router, providing the same great developer experience across all Next.js versions. # Nitro/H3 (/docs/integrations/nitro-h3) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; ## Using pushduck with Nitro/H3 Nitro is a universal web server framework that powers Nuxt.js, built on top of H3 (HTTP framework). It uses Web Standards APIs and provides excellent performance with universal deployment. Since Nitro/H3 uses standard `Request`/`Response` objects, pushduck handlers work directly without any adapters! **Web Standards Native**: Nitro/H3 uses Web Standard `Request`/`Response` objects, making pushduck integration straightforward with zero overhead and universal deployment capabilities. ## Quick Setup **Install dependencies** ```bash npm install pushduck ``` ```bash yarn add pushduck ``` ```bash pnpm add pushduck ``` ```bash bun add pushduck ``` **Configure upload router** ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create API route** ```typescript title="routes/api/upload/[...path].ts" import { uploadRouter } from '~/lib/upload'; // Direct usage - no adapter needed! export default defineEventHandler(async (event) => { return uploadRouter.handlers(event.node.req); }); ``` ## Basic Integration ### Simple Upload Route ```typescript title="routes/api/upload/[...path].ts" import { uploadRouter } from '~/lib/upload'; // Method 1: Combined handler (recommended) export default defineEventHandler(async (event) => { return uploadRouter.handlers(event.node.req); }); // Method 2: Method-specific handlers export default defineEventHandler(async (event) => { const method = getMethod(event); if (method === 'GET') { return uploadRouter.handlers.GET(event.node.req); } if (method === 'POST') { return uploadRouter.handlers.POST(event.node.req); } throw createError({ statusCode: 405, statusMessage: 'Method Not Allowed' }); }); ``` ### With H3 Utilities ```typescript title="routes/api/upload/[...path].ts" import { uploadRouter } from '~/lib/upload'; import { defineEventHandler, getMethod, setHeader, createError } from 'h3'; export default defineEventHandler(async (event) => { // Handle CORS setHeader(event, 'Access-Control-Allow-Origin', '*'); setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type'); // Handle preflight requests if (getMethod(event) === 'OPTIONS') { return ''; } try { return await uploadRouter.handlers(event.node.req); } catch (error) { throw createError({ statusCode: 500, statusMessage: 'Upload failed', data: error }); } }); ``` ## Advanced Configuration ### Authentication with H3 ```typescript title="lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; import { getCookie } from 'h3'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with session authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const cookies = req.headers.cookie; const sessionId = parseCookie(cookies)?.sessionId; if (!sessionId) { throw new Error('Authentication required'); } const user = await getUserFromSession(sessionId); if (!user) { throw new Error('Invalid session'); } return { userId: user.id, username: user.username, }; }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; // Helper functions function parseCookie(cookieString: string | undefined) { if (!cookieString) return {}; return Object.fromEntries( cookieString.split('; ').map(c => { const [key, ...v] = c.split('='); return [key, v.join('=')]; }) ); } async function getUserFromSession(sessionId: string) { // Implement your session validation logic return { id: 'user-123', username: 'demo-user' }; } ``` ## Standalone Nitro App ### Basic Nitro Setup ```typescript title="nitro.config.ts" export default defineNitroConfig({ srcDir: 'server', routeRules: { '/api/upload/**': { cors: true, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' } } }, experimental: { wasm: true } }); ``` ### Server Entry Point ```typescript title="server/index.ts" import { createApp, toNodeListener } from 'h3'; import { uploadRouter } from './lib/upload'; const app = createApp(); // Upload routes app.use('/api/upload/**', defineEventHandler(async (event) => { return uploadRouter.handlers(event.node.req); })); // Health check app.use('/health', defineEventHandler(() => ({ status: 'ok' }))); export default toNodeListener(app); ``` ## Client-Side Usage ### HTML with Vanilla JavaScript ```html title="public/index.html" File Upload Demo

File Upload Demo

Image Upload

Document Upload

``` ### With Framework Integration ```typescript title="plugins/upload.client.ts" import { useUpload } from "pushduck/client"; import type { AppUploadRouter } from "~/lib/upload"; export const { UploadButton, UploadDropzone } = useUpload({ endpoint: "/api/upload", }); ``` ## File Management ### File API Route ```typescript title="routes/api/files.get.ts" import { defineEventHandler, getQuery, createError } from 'h3'; export default defineEventHandler(async (event) => { const query = getQuery(event); const userId = query.userId as string; if (!userId) { throw createError({ statusCode: 400, statusMessage: 'User ID required' }); } // Fetch files from database const files = await getFilesForUser(userId); return { files: files.map(file => ({ id: file.id, name: file.name, url: file.url, size: file.size, uploadedAt: file.createdAt, })), }; }); async function getFilesForUser(userId: string) { // Implement your database query logic return []; } ``` ### File Management Page ```html title="public/files.html" My Files

My Files

Uploaded Files

``` ## Deployment Options ```typescript title="nitro.config.ts" export default defineNitroConfig({ preset: 'vercel-edge', // or 'vercel' for Node.js runtime }); ``` ```typescript title="nitro.config.ts" export default defineNitroConfig({ preset: 'netlify-edge', // or 'netlify' for Node.js runtime }); ``` ```typescript title="nitro.config.ts" export default defineNitroConfig({ preset: 'node-server', }); ``` ```typescript title="nitro.config.ts" export default defineNitroConfig({ preset: 'cloudflare-workers', }); ``` ## Environment Variables ```bash title=".env" # AWS Configuration AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_S3_BUCKET=your-bucket-name # Nitro NITRO_PORT=3000 NITRO_HOST=0.0.0.0 ``` ## Performance Benefits ## Middleware and Plugins ```typescript title="middleware/cors.ts" export default defineEventHandler(async (event) => { if (event.node.req.url?.startsWith('/api/upload')) { setHeader(event, 'Access-Control-Allow-Origin', '*'); setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type'); if (getMethod(event) === 'OPTIONS') { return ''; } } }); ``` ```typescript title="plugins/database.ts" export default async (nitroApp) => { // Initialize database connection console.log('Database plugin initialized'); // Add database to context nitroApp.hooks.hook('request', async (event) => { event.context.db = await getDatabase(); }); }; ``` ## Real-Time Upload Progress ```html title="public/advanced-upload.html" Advanced Upload
``` ## Troubleshooting **Common Issues** 1. **Route not found**: Ensure your route is `routes/api/upload/[...path].ts` 2. **Build errors**: Check that pushduck and h3 are properly installed 3. **CORS issues**: Use Nitro's built-in CORS handling or middleware 4. **Environment variables**: Make sure they're accessible in your deployment environment ### Debug Mode Enable debug logging: ```typescript title="lib/upload.ts" export const uploadRouter = createS3Router({ // ... routes }).middleware(async ({ req, file }) => { if (process.env.NODE_ENV === "development") { console.log("Upload request:", req.url); console.log("File:", file.name, file.size); } return {}; }); ``` ### Nitro Configuration ```typescript title="nitro.config.ts" export default defineNitroConfig({ srcDir: 'server', buildDir: '.nitro', output: { dir: '.output', serverDir: '.output/server', publicDir: '.output/public' }, runtimeConfig: { awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: process.env.AWS_REGION, s3BucketName: process.env.S3_BUCKET_NAME, }, experimental: { wasm: true } }); ``` Nitro/H3 provides an excellent foundation for building universal web applications with pushduck, offering flexibility, performance, and deployment options across any platform while maintaining full compatibility with Web Standards APIs. # Nuxt.js (/docs/integrations/nuxtjs) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; **🚧 Client-Side In Development**: Nuxt.js server-side integration is fully functional with Web Standards APIs. However, Nuxt.js-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now. ## Using pushduck with Nuxt.js Nuxt.js is the intuitive Vue.js framework for building full-stack web applications. It uses Web Standards APIs and provides excellent performance with server-side rendering. Since Nuxt.js uses standard `Request`/`Response` objects, pushduck handlers work directly without any adapters! **Web Standards Native**: Nuxt.js server routes use Web Standard `Request`/`Response` objects, making pushduck integration straightforward with zero overhead. ## Quick Setup **Install dependencies** ```bash npm install pushduck ``` **Configure upload router** ```typescript title="server/utils/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create API route** ```typescript title="server/api/upload/[...path].ts" import { uploadRouter } from '~/server/utils/upload'; // Direct usage - no adapter needed! export default defineEventHandler(async (event) => { return uploadRouter.handlers(event.node.req); }); ``` ## Basic Integration ### Simple Upload Route ```typescript title="server/api/upload/[...path].ts" import { uploadRouter } from '~/server/utils/upload'; // Method 1: Combined handler (recommended) export default defineEventHandler(async (event) => { return uploadRouter.handlers(event.node.req); }); // Method 2: Method-specific handlers export default defineEventHandler({ onRequest: [ // Add middleware here if needed ], handler: async (event) => { if (event.node.req.method === 'GET') { return uploadRouter.handlers.GET(event.node.req); } if (event.node.req.method === 'POST') { return uploadRouter.handlers.POST(event.node.req); } } }); ``` ### With Server Middleware ```typescript title="server/middleware/cors.ts" export default defineEventHandler(async (event) => { if (event.node.req.url?.startsWith('/api/upload')) { // Handle CORS for upload routes setHeader(event, 'Access-Control-Allow-Origin', '*'); setHeader(event, 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); setHeader(event, 'Access-Control-Allow-Headers', 'Content-Type'); if (event.node.req.method === 'OPTIONS') { return ''; } } }); ``` ## Advanced Configuration ### Authentication with Nuxt ```typescript title="server/utils/upload.ts" import { createUploadConfig } from 'pushduck/server'; import jwt from 'jsonwebtoken'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: process.env.AWS_ENDPOINT_URL!, bucket: process.env.S3_BUCKET_NAME!, accountId: process.env.R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with JWT authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { throw new Error('Authorization required'); } const token = authHeader.substring(7); try { const payload = jwt.verify(token, process.env.JWT_SECRET!) as any; return { userId: payload.sub, userRole: payload.role }; } catch (error) { throw new Error('Invalid token'); } }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; ``` ## Client-Side Usage ### Upload Composable ```typescript title="composables/useUpload.ts" import { useUpload } from "pushduck/client"; import type { AppUploadRouter } from "~/server/utils/upload"; export const { UploadButton, UploadDropzone } = useUpload({ endpoint: "/api/upload", }); ``` ### Upload Component ```vue title="components/FileUpload.vue" ``` ### Using in Pages ```vue title="pages/index.vue" ``` ## File Management ### Server-Side File API ```typescript title="server/api/files.get.ts" export default defineEventHandler(async (event) => { const query = getQuery(event); const userId = query.userId as string; if (!userId) { throw createError({ statusCode: 400, statusMessage: 'User ID required' }); } // Fetch files from database const files = await $fetch('/api/database/files', { query: { userId } }); return { files: files.map((file: any) => ({ id: file.id, name: file.name, url: file.url, size: file.size, uploadedAt: file.createdAt, })), }; }); ``` ### File Management Page ```vue title="pages/files.vue" ``` ## Deployment Options ```typescript title="nuxt.config.ts" export default defineNuxtConfig({ nitro: { preset: 'vercel-edge', // or 'vercel' for Node.js runtime }, runtimeConfig: { awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: process.env.AWS_REGION, s3BucketName: process.env.S3_BUCKET_NAME, } }); ``` ```typescript title="nuxt.config.ts" export default defineNuxtConfig({ nitro: { preset: 'netlify-edge', // or 'netlify' for Node.js runtime }, runtimeConfig: { awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: process.env.AWS_REGION, s3BucketName: process.env.S3_BUCKET_NAME, } }); ``` ```typescript title="nuxt.config.ts" export default defineNuxtConfig({ nitro: { preset: 'node-server', }, runtimeConfig: { awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: process.env.AWS_REGION, s3BucketName: process.env.S3_BUCKET_NAME, } }); ``` ```typescript title="nuxt.config.ts" export default defineNuxtConfig({ nitro: { preset: 'cloudflare-pages', }, runtimeConfig: { awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID, awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, awsRegion: process.env.AWS_REGION, s3BucketName: process.env.S3_BUCKET_NAME, } }); ``` ## Environment Variables ```bash title=".env" # AWS Configuration AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_S3_BUCKET=your-bucket-name # JWT Secret (for authentication) JWT_SECRET=your-jwt-secret # Nuxt NUXT_PUBLIC_UPLOAD_ENDPOINT=http://localhost:3000/api/upload ``` ## Performance Benefits ## Real-Time Upload Progress ```vue title="components/AdvancedUpload.vue" ``` ## Troubleshooting **Common Issues** 1. **Route not found**: Ensure your route is `server/api/upload/[...path].ts` 2. **Build errors**: Check that pushduck is properly installed 3. **CORS issues**: Use server middleware for CORS configuration 4. **Runtime config**: Make sure environment variables are properly configured ### Debug Mode Enable debug logging: ```typescript title="server/utils/upload.ts" export const uploadRouter = createS3Router({ // ... routes }).middleware(async ({ req, file }) => { if (process.dev) { console.log("Upload request:", req.url); console.log("File:", file.name, file.size); } return {}; }); ``` ### Nitro Configuration ```typescript title="nuxt.config.ts" export default defineNuxtConfig({ nitro: { experimental: { wasm: true }, // Enable debugging in development devProxy: { '/api/upload': { target: 'http://localhost:3000/api/upload', changeOrigin: true } } } }); ``` Nuxt.js provides an excellent foundation for building full-stack Vue.js applications with pushduck, combining the power of Vue's reactive framework with Web Standards APIs and Nitro's universal deployment capabilities. # Qwik (/docs/integrations/qwik) import { Callout } from "fumadocs-ui/components/callout"; import { Card, Cards } from "fumadocs-ui/components/card"; import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import { Steps, Step } from "fumadocs-ui/components/steps"; import { File, Folder, Files } from "fumadocs-ui/components/files"; **🚧 Client-Side In Development**: Qwik server-side integration is fully functional with Web Standards APIs. However, Qwik-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now. ## Using pushduck with Qwik Qwik is a web framework focused on resumability and edge optimization. It uses Web Standards APIs and provides instant loading with minimal JavaScript. Since Qwik uses standard `Request`/`Response` objects, pushduck handlers work directly without any adapters! **Web Standards Native**: Qwik server endpoints use Web Standard `Request`/`Response` objects, making pushduck integration straightforward with zero overhead and perfect for edge deployment. ## Quick Setup **Install dependencies** ```bash npm install pushduck ``` **Configure upload router** ```typescript title="src/lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID!, secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: import.meta.env.VITE_AWS_ENDPOINT_URL!, bucket: import.meta.env.VITE_S3_BUCKET_NAME!, accountId: import.meta.env.VITE_R2_ACCOUNT_ID!, }) .build(); export const uploadRouter = createS3Router({ imageUpload: s3.image().maxFileSize("5MB"), documentUpload: s3.file().maxFileSize("10MB") }); export type AppUploadRouter = typeof uploadRouter; ``` **Create API route** ```typescript title="src/routes/api/upload/[...path]/index.ts" import type { RequestHandler } from '@builder.io/qwik-city'; import { uploadRouter } from '~/lib/upload'; // Direct usage - no adapter needed! export const onGet: RequestHandler = async ({ request }) => { return uploadRouter.handlers(request); }; export const onPost: RequestHandler = async ({ request }) => { return uploadRouter.handlers(request); }; ``` ## Basic Integration ### Simple Upload Route ```typescript title="src/routes/api/upload/[...path]/index.ts" import type { RequestHandler } from '@builder.io/qwik-city'; import { uploadRouter } from '~/lib/upload'; // Method 1: Combined handler (recommended) export const onRequest: RequestHandler = async ({ request }) => { return uploadRouter.handlers(request); }; // Method 2: Separate handlers (if you need method-specific logic) export const onGet: RequestHandler = async ({ request }) => { return uploadRouter.handlers.GET(request); }; export const onPost: RequestHandler = async ({ request }) => { return uploadRouter.handlers.POST(request); }; ``` ### With CORS Support ```typescript title="src/routes/api/upload/[...path]/index.ts" import type { RequestHandler } from '@builder.io/qwik-city'; import { uploadRouter } from '~/lib/upload'; export const onRequest: RequestHandler = async ({ request, headers }) => { // Handle CORS preflight if (request.method === 'OPTIONS') { headers.set('Access-Control-Allow-Origin', '*'); headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); headers.set('Access-Control-Allow-Headers', 'Content-Type'); return new Response(null, { status: 200 }); } const response = await uploadRouter.handlers(request); // Add CORS headers to actual response headers.set('Access-Control-Allow-Origin', '*'); return response; }; ``` ## Advanced Configuration ### Authentication with Qwik ```typescript title="src/lib/upload.ts" import { createUploadConfig } from 'pushduck/server'; const { s3, createS3Router } = createUploadConfig() .provider("cloudflareR2",{ accessKeyId: import.meta.env.VITE_AWS_ACCESS_KEY_ID!, secretAccessKey: import.meta.env.VITE_AWS_SECRET_ACCESS_KEY!, region: 'auto', endpoint: import.meta.env.VITE_AWS_ENDPOINT_URL!, bucket: import.meta.env.VITE_S3_BUCKET_NAME!, accountId: import.meta.env.VITE_R2_ACCOUNT_ID!, }) .paths({ prefix: 'uploads', generateKey: (file, metadata) => { return `${metadata.userId}/${Date.now()}/${file.name}`; } }) .build(); export const uploadRouter = createS3Router({ // Private uploads with cookie-based authentication privateUpload: s3 .image() .maxFileSize("5MB") .middleware(async ({ req }) => { const cookies = req.headers.get('Cookie'); const sessionId = parseCookie(cookies)?.sessionId; if (!sessionId) { throw new Error('Authentication required'); } const user = await getUserFromSession(sessionId); if (!user) { throw new Error('Invalid session'); } return { userId: user.id, username: user.username, }; }), // Public uploads (no auth) publicUpload: s3 .image() .maxFileSize("2MB") // No middleware = public access }); export type AppUploadRouter = typeof uploadRouter; // Helper functions function parseCookie(cookieString: string | null) { if (!cookieString) return {}; return Object.fromEntries( cookieString.split('; ').map(c => { const [key, ...v] = c.split('='); return [key, v.join('=')]; }) ); } async function getUserFromSession(sessionId: string) { // Implement your session validation logic return { id: 'user-123', username: 'demo-user' }; } ``` ## Client-Side Usage ### Upload Component ```tsx title="src/components/file-upload.tsx" import { component$, useSignal } from '@builder.io/qwik'; import { useUpload } from "pushduck/client"; import type { AppUploadRouter } from "~/lib/upload"; export const FileUpload = component$(() => { const uploadProgress = useSignal(0); const isUploading = useSignal(false); const { UploadButton, UploadDropzone } = useUpload({ endpoint: "/api/upload", }); const handleUploadComplete = $((files: any[]) => { console.log("Files uploaded:", files); alert("Upload completed!"); }); const handleUploadError = $((error: Error) => { console.error("Upload error:", error); alert(`Upload failed: ${error.message}`); }); return (

Image Upload

Document Upload

); }); ``` ### Using in Routes ```tsx title="src/routes/index.tsx" import { component$ } from '@builder.io/qwik'; import type { DocumentHead } from '@builder.io/qwik-city'; import { FileUpload } from '~/components/file-upload'; export default component$(() => { return (

File Upload Demo

); }); export const head: DocumentHead = { title: 'File Upload Demo', meta: [ { name: 'description', content: 'Qwik file upload demo with pushduck', }, ], }; ``` ## File Management ### Server-Side File Loader ```typescript title="src/routes/files/index.tsx" import { component$ } from '@builder.io/qwik'; import type { DocumentHead } from '@builder.io/qwik-city'; import { routeLoader$ } from '@builder.io/qwik-city'; import { FileUpload } from '~/components/file-upload'; export const useFiles = routeLoader$(async (requestEvent) => { const userId = 'current-user'; // Get from session/auth // Fetch files from database const files = await getFilesForUser(userId); return { files: files.map(file => ({ id: file.id, name: file.name, url: file.url, size: file.size, uploadedAt: file.createdAt, })), }; }); export default component$(() => { const filesData = useFiles(); const formatFileSize = (bytes: number): string => { const sizes = ['Bytes', 'KB', 'MB', 'GB']; if (bytes === 0) return '0 Bytes'; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; }; return (

My Files

Uploaded Files

{filesData.value.files.length === 0 ? (

No files uploaded yet.

) : (
{filesData.value.files.map((file) => (

{file.name}

{formatFileSize(file.size)}

{new Date(file.uploadedAt).toLocaleDateString()}

View File
))}
)}
); }); export const head: DocumentHead = { title: 'My Files', }; async function getFilesForUser(userId: string) { // Implement your database query logic return []; } ``` ## Deployment Options ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; import { qwikVite } from '@builder.io/qwik/optimizer'; import { qwikCity } from '@builder.io/qwik-city/vite'; import { qwikCloudflarePages } from '@builder.io/qwik-city/adapters/cloudflare-pages/vite'; export default defineConfig(() => { return { plugins: [ qwikCity({ adapter: qwikCloudflarePages(), }), qwikVite(), ], }; }); ``` ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; import { qwikVite } from '@builder.io/qwik/optimizer'; import { qwikCity } from '@builder.io/qwik-city/vite'; import { qwikVercel } from '@builder.io/qwik-city/adapters/vercel-edge/vite'; export default defineConfig(() => { return { plugins: [ qwikCity({ adapter: qwikVercel(), }), qwikVite(), ], }; }); ``` ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; import { qwikVite } from '@builder.io/qwik/optimizer'; import { qwikCity } from '@builder.io/qwik-city/vite'; import { qwikNetlifyEdge } from '@builder.io/qwik-city/adapters/netlify-edge/vite'; export default defineConfig(() => { return { plugins: [ qwikCity({ adapter: qwikNetlifyEdge(), }), qwikVite(), ], }; }); ``` ```typescript title="vite.config.ts" import { defineConfig } from 'vite'; import { qwikVite } from '@builder.io/qwik/optimizer'; import { qwikCity } from '@builder.io/qwik-city/vite'; import { qwikDeno } from '@builder.io/qwik-city/adapters/deno/vite'; export default defineConfig(() => { return { plugins: [ qwikCity({ adapter: qwikDeno(), }), qwikVite(), ], }; }); ``` ## Environment Variables ```bash title=".env" # AWS Configuration VITE_AWS_REGION=us-east-1 VITE_AWS_ACCESS_KEY_ID=your_access_key VITE_AWS_SECRET_ACCESS_KEY=your_secret_key VITE_AWS_S3_BUCKET=your-bucket-name # Qwik VITE_PUBLIC_UPLOAD_ENDPOINT=http://localhost:5173/api/upload ``` ## Performance Benefits ## Real-Time Upload Progress ```tsx title="src/components/advanced-upload.tsx" import { component$, useSignal, $ } from '@builder.io/qwik'; export const AdvancedUpload = component$(() => { const uploadProgress = useSignal(0); const isUploading = useSignal(false); const handleFileUpload = $(async (event: Event) => { const target = event.target as HTMLInputElement; const files = target.files; if (!files || files.length === 0) return; isUploading.value = true; uploadProgress.value = 0; try { // Simulate upload progress for (let i = 0; i <= 100; i += 10) { uploadProgress.value = i; await new Promise(resolve => setTimeout(resolve, 100)); } alert('Upload completed!'); } catch (error) { console.error('Upload failed:', error); alert('Upload failed!'); } finally { isUploading.value = false; uploadProgress.value = 0; } }); return (
{isUploading.value && (

{uploadProgress.value}% uploaded

)}
); }); ``` ## Qwik City Form Integration ```tsx title="src/routes/upload-form/index.tsx" import { component$ } from '@builder.io/qwik'; import type { DocumentHead } from '@builder.io/qwik-city'; import { routeAction$, Form, zod$, z } from '@builder.io/qwik-city'; import { FileUpload } from '~/components/file-upload'; export const useUploadAction = routeAction$(async (data, requestEvent) => { // Handle form submission // Files are already uploaded via pushduck, just save metadata console.log('Form data:', data); // Redirect to files page throw requestEvent.redirect(302, '/files'); }, zod$({ title: z.string().min(1), description: z.string().optional(), })); export default component$(() => { const uploadAction = useUploadAction(); return (

Upload Files