Pushduck

repository·main·Indexed 42 days ago

https://github.com/abhay-ramesh/pushduck

A lightweight, universal S3 file upload library for web applications. It provides a type-safe client for secure file uploads with minimal boilerplate and no heavy AWS SDK dependencies. Features include a CLI for guided project initialization, a conformance suite for server implementations, and an enhanced property-based upload client for superior TypeScript inference and IntelliSense.

Tokens
218.1K
Snippets
579
Records
725
Agent score
50%

What's inside Pushduck

  1. Compare Pushduck with other file upload solutions

    main

    Pushduck is a lightweight, self-hosted solution designed for developers who want full control over their S3 storage without vendor lock-in or ongoing upload fees. It is particularly well-suited for projects requiring small bundle sizes, edge runtime support, and multi-provider compatibility (AWS, R2, DO, MinIO, GCS, and S3-compat).

    Quick Comparison Summary

    FeaturePushduckUploadThingUploadcareAWS SDKUppy
    Bundle Size~7KB~200KB+~150KB+~500KB+~50KB+ (core)
    Setup Time5 minutes10 minutes15 minutes15-20 hours30-60 minutes
    Edge Runtime✅ Yes✅ Yes✅ Yes❌ No✅ Partial
    Self-Hosted✅ Yes❌ No❌ No✅ Yes✅ Yes
    Pricing ModelFree (S3 costs only)Per uploadPer upload + storageFree (S3 costs only)Free (S3 costs only)
    Type Safety✅ Full✅ Full⚠️ Partial⚠️ Manual❌ None
    Multi-Provider✅ 6❌ Own infra❌ Own infra❌ 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✅ AutomaticN/A❌ Build yourself⚠️ Manual
  2. Core features of Pushduck

    main

    Pushduck is a universal S3 file upload library with the following core capabilities:

    • Web-standard Compatibility: Uses Request/Response handlers, making it compatible with any WinterCG runtime (Next.js, Hono, Bun, Deno, Cloudflare Workers, etc.) without dedicated adapters.
    • Typesafe Routing: Includes a typesafe router with InferClientRouter<typeof router> for sharing types between server and client in TS monorepos.
    • Middleware & Lifecycle: Supports a middleware chain with typed metadata and lifecycle hooks: onStart, onProgress, onSuccess, onError, and onUploadComplete.
    • Schema Validation: Provides a schema chain for defining upload constraints: s3.image(), s3.file(), .accept(), .maxFileSize(), .maxFiles(), .middleware(), .paths(), and .onUploadComplete().
    • Progress Tracking: Supports per-file and aggregate progress tracking (percentage, upload speed, and ETA).
    • Metadata Passthrough: Allows attaching client-side application metadata via uploadFiles(files, metadata), which is accessible in server middleware and lifecycle hooks.
  3. Core Upload and Storage Features

    main

    Pushduck is a focused library designed for S3 file uploads. Its core capabilities include:

    Core Upload Features

    • Direct-to-S3 Uploads: Secure presigned URL generation for uploading files directly to S3, bypassing your server to reduce bandwidth and improve performance.
    • Progress Tracking: Real-time monitoring of upload progress, speed, and ETA (supports both per-file and overall metrics for multi-file uploads).
    • File Validation: Client and server-side checks for file size, type, count, and custom rules.
    • Multi-Provider Support: Compatible with AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO, and any S3-compatible provider.

    Storage Operations

    Pushduck provides a set of storage management APIs for interacting with your files:

    • Listing: List files with pagination and filtering.
    • Deletion: Delete single files, batch files, or files matching a specific prefix.
    • Metadata: Retrieve file information such as size, date, and content-type.
    • Presigned URLs: Generate URLs for both uploading and downloading files.
    • Existence Checks: Verify if a file exists in storage.
  4. Advanced GCS configurations: Multi-regional, Storage Classes, and CDN

    main

    Pushduck allows fine-grained control over GCS features through the provider configuration.

    Multi-Regional Storage

    Set the location and storageClass to optimize for global performance.

    CDN Integration

    Use cdnUrl and defaultCacheControl to serve files via a CDN.

    Storage Classes

    Use lifecycle hooks to move files to cheaper storage classes (e.g., COLDLINE, ARCHIVE) based on metadata.

    export const { s3 } = createUploadConfig()
      .provider("gcs", {
        projectId: process.env.GCS_PROJECT_ID!,
        keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS!,
        bucket: process.env.GCS_BUCKET_NAME!,
        location: "US", // or "EU", "ASIA"
        storageClass: "MULTI_REGIONAL",
        cdnUrl: "https://your-cdn-domain.com",
        defaultCacheControl: "public, max-age=31536000",
      })
      .build();
  5. How multipart uploads work in the Pushduck protocol

    main

    Multipart uploads allow splitting large files (up to 5 GB per single PUT) into independent parts that can be uploaded in parallel and retried individually.

    Key Concepts

    • Discovery: A client discovers multipart support by attempting multipart-init. If the server returns a 400, the client must fall back to a single PUT or fail.
    • Sessions: The init action returns an opaque session ID. All subsequent multipart actions (multipart-sign, multipart-complete, etc.) must use this session instead of a key or upload ID. This ensures the server can authenticate the caller and derive the object key securely from the session.
    • Authoritative Part Size: The server's partSize returned in the multipart-init response is authoritative. Clients must use this value to compute part boundaries and must not use their own preferred size.
    • Part Sizing Rules:
      • Minimum part size: 5 MiB.
      • Maximum part size: 5 GiB.
      • Maximum parts per object: 10,000.
      • Uniformity: Every part except the last MUST be the same size to ensure compatibility with providers like Cloudflare R2.

    Multipart Workflow

    1. multipart-init: Initializes the upload and returns a session and authoritative partSize.
    2. multipart-sign: Requests signed URLs for specific partNumbers using the session.
    3. multipart-complete: Finalizes the upload by providing the etag for each part and the file metadata.
    4. multipart-abort: (Optional) Discards the session and its parts to avoid being billed for abandoned parts.
    5. multipart-parts: (Optional) Allows a client to resume an upload by reporting which parts the provider actually holds.
    // multipart-init request example
    {
      "file": { "name": "video.mp4", "size": 524288000, "type": "video/mp4" },
      "metadata": { "albumId": "summer-2026" },   // optional, UNTRUSTED
      "partSize": 5242880                          // optional, a preference
    }
    
    // multipart-init response 200 example
    {
      "success": true,
      "session": "…",                 // opaque
      "key": "uploads/2026/video.mp4",
      "partSize": 5242880,            // authoritative
      "metadata": { "userId": "u_1" }
    }
  6. Secure file uploads with middleware

    main

    Pushduck allows you to secure upload endpoints by attaching a .middleware() function to your file definitions within an s3.createRouter().

    Inside the middleware, you can inspect the request (req) and existing metadata. If authentication fails, you should throw an error. If it succeeds, you must return an object that merges the existing metadata with new properties (e.g., userId, roles, plan). These returned properties are then attached to the file's metadata in S3, allowing you to track ownership or usage limits.

    Warning: Never expose upload endpoints without authentication in production to prevent storage abuse.

    const s3Router = s3.createRouter({
      userFiles: s3.image()
        .middleware(async ({ req, metadata }) => {
          // 1. Perform auth check
          // 2. Throw error if failed
          // 3. Return merged metadata
          return { ...metadata, userId: 'user_123' };
        }),
    });
  7. How storage providers work in Pushduck

    main

    Pushduck uses a Universal S3 API to connect to various cloud storage providers. This means all providers follow the same configuration pattern, allowing you to switch between providers (e.g., moving from AWS S3 to Cloudflare R2) or use multiple providers within a single project by simply changing the provider key in your configuration. This abstraction provides vendor independence and simplifies infrastructure management.

    import { createS3Router, s3 } from 'pushduck/server';
    
    const uploadRouter = createS3Router({
      storage: {
        provider: 'aws-s3', // Switching providers only requires changing this key and relevant credentials
        region: 'us-east-1',
        bucket: 'your-bucket-name',
        credentials: {
          accessKeyId: process.env.ACCESS_KEY_ID!,
          secretAccessKey: process.env.SECRET_ACCESS_KEY!,
        },
      },
      // ...
    });
  8. Decision Matrix: When to use Pushduck vs alternatives

    main

    Use Pushduck if you require full control over storage, need edge runtime compatibility (Vercel, Cloudflare Workers), prefer self-hosting to minimize costs (paying only S3 fees), or want to avoid vendor lock-in. It is specifically optimized for TypeScript, React, and Next.js environments.

    Comparison Summary:

    • Pushduck: Best for full control, edge compatibility, low cost (S3 fees only), and multi-provider S3 support (R2, DigitalOcean, MinIO, etc.).
    • UploadThing: Best for rapid prototyping where you want zero infrastructure setup and are okay with managed service pricing.
    • AWS SDK: Best for AWS-only environments requiring advanced S3 features (lifecycle, versioning) and very large file multipart uploads, provided you don't need edge runtime support and can manage the complex implementation yourself.
    • Uploadcare / Filestack: Best for all-in-one solutions including built-in image/video processing and global CDNs, if you have a higher budget for managed services.
    • Uppy: Best for rich, pre-built UI components, highly customizable widgets, or when using non-React frameworks (Vue, Svelte, Vanilla JS).
  9. Implement Authentication and Authorization via Middleware

    main

    Pushduck is a library, not a SaaS, so it does not include built-in user management or authentication. Instead, it provides a .middleware() hook that allows you to integrate your existing authentication and authorization systems (e.g., NextAuth, Clerk, Supabase, or custom solutions).

    Inside the middleware, you can inspect the request (req), validate the user, check permissions, and return metadata (like userId) that will be available in subsequent lifecycle hooks.

    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 };
        })
    });
  10. How Pushduck's component architecture is organized

    main

    Pushduck is divided into three main layers:

    1. Client Layer (pushduck)

    Provides the user interface logic:

    • useUploadRoute: A React hook for managing upload state.
    • createUploadClient: A property-based API for enhanced client functionality.
    • Utilities: Helpers like formatETA and formatSpeed for UI feedback.

    2. Server Layer (pushduck/server)

    Handles the backend logic and configuration:

    • createUploadConfig: A builder pattern for setting up the server.
    • Schemas: Pre-defined schemas like s3.image or s3.file.
    • Router: Created via createRouter to define upload routes.
    • Providers: Implementations for AWS, R2, DigitalOcean, etc.
    • Storage API: Created via createStorage to interact with providers.

    3. Core System

    The underlying engine:

    • Universal Handler: Uses Web Standards to remain framework-agnostic.
    • S3 Client: Powered by aws4fetch (lightweight and edge-compatible).
    • Middleware Chain & Lifecycle Hooks: For extensibility.

    4. Framework Adapters

    Thin layers that allow the Universal Handler to work with frameworks like Next.js, Express, and Fastify.

  11. Authenticate uploads using SvelteKit middleware

    main

    You can secure specific upload routes by attaching .middleware() to an upload definition in your router configuration. The middleware receives the request and can return metadata (like userId) that will be available during the key generation process via .paths({ generateKey: ... }).

    Security Warning: Client-side metadata passed to uploadFiles is untrusted. Always derive identity (like userId) from the authenticated session within the server-side middleware.

    import { createUploadConfig } from 'pushduck/server';
    import { getUserFromSession } 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();
    
    export const uploadRouter = createS3Router({
      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,
          };
        }),
    
      publicUpload: s3
        .image()
        .maxFileSize("2MB")
    });