Upload Configuration

Complete guide to configuring pushduck with the createUploadConfig builder

Router Configuration Options

The createUploadConfig() builder provides a fluent API for configuring your S3 uploads with providers, security, paths, and lifecycle hooks.

Basic Setup

// lib/upload.ts
import { createUploadConfig } from 'pushduck/server'

const { storage, s3, config } = 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 { storage, s3 }

Provider Configuration

The createUploadConfig().provider() method provides full TypeScript type safety for provider configurations. When you specify a provider type, TypeScript will automatically infer the correct configuration interface and provide autocomplete and type checking.

Features

  • Provider name autocomplete: TypeScript suggests valid provider names
  • Configuration property validation: Only valid properties for each provider are accepted
  • Required field checking: TypeScript ensures all required fields are provided
  • Property type validation: Each property must be the correct type

Provider Configuration

Cloudflare R2

createUploadConfig().provider("cloudflareR2",{
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: 'auto', // Always 'auto' for R2
  endpoint: process.env.AWS_ENDPOINT_URL,
  bucket: process.env.S3_BUCKET_NAME,
  accountId: process.env.R2_ACCOUNT_ID, // Required for R2
  // Optional: Custom domain for public files
  customDomain: process.env.R2_CUSTOM_DOMAIN,
})

AWS S3

createUploadConfig().provider("aws",{
  region: 'us-east-1',
  bucket: 'your-bucket',
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  // Optional: Custom domain for public files (CDN, CloudFront, etc.)
  customDomain: process.env.S3_CUSTOM_DOMAIN,
})

DigitalOcean Spaces

createUploadConfig().provider("digitalOceanSpaces",{
  region: 'nyc3',
  bucket: 'your-space',
  accessKeyId: process.env.DO_SPACES_ACCESS_KEY_ID,
  secretAccessKey: process.env.DO_SPACES_SECRET_ACCESS_KEY,
  // Optional: Custom domain for public files (CDN endpoint)
  customDomain: process.env.DO_SPACES_CUSTOM_DOMAIN,
})

MinIO

createUploadConfig().provider("minio",{
  endpoint: 'localhost:9000',
  bucket: 'your-bucket',
  accessKeyId: process.env.MINIO_ACCESS_KEY_ID,
  secretAccessKey: process.env.MINIO_SECRET_ACCESS_KEY,
  useSSL: false,
  // Optional: Custom domain for public files
  customDomain: process.env.MINIO_CUSTOM_DOMAIN,
})

S3-Compatible (Generic)

For any S3-compatible storage service not explicitly supported:

createUploadConfig().provider("s3Compatible",{
  endpoint: 'https://your-s3-compatible-service.com',
  bucket: 'your-bucket',
  accessKeyId: process.env.S3_ACCESS_KEY_ID,
  secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  region: 'us-east-1', // Optional, defaults to us-east-1
  forcePathStyle: true, // Optional, defaults to true for compatibility
  // Optional: Custom domain for public files
  customDomain: process.env.S3_CUSTOM_DOMAIN,
})

Configuration Methods

.defaults()

Set default file constraints and options:

.defaults({
  maxFileSize: '10MB',
  allowedFileTypes: ['image/*', 'application/pdf', 'text/*'],
  acl: 'public-read', // or 'private'
  metadata: {
    uploadedBy: 'system',
    environment: process.env.NODE_ENV,
  },
})

.paths()

Configure global path structure:

.paths({
  // Global prefix for all uploads
  prefix: 'uploads',
  
  // Global key generation function
  generateKey: (file, metadata) => {
    const userId = metadata.userId || 'anonymous'
    const timestamp = Date.now()
    const randomId = Math.random().toString(36).substring(2, 8)
    const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_')
    
    return `${userId}/${timestamp}/${randomId}/${sanitizedName}`
  },
})

.security()

Configure security and access control:

.security({
  requireAuth: true,
  allowedOrigins: [
    'http://localhost:3000',
    'https://your-domain.com',
  ],
  rateLimiting: {
    maxUploads: 10,
    windowMs: 60000, // 1 minute
  },
})

.hooks()

Add lifecycle hooks for upload events:

.hooks({
  onUploadStart: async ({ file, metadata }) => {
    console.log(`🚀 Upload started: ${file.name}`)
    // Log to analytics, validate user, etc.
  },
  
  onUploadComplete: async ({ file, url, metadata }) => {
    console.log(`✅ Upload completed: ${file.name} -> ${url}`)
    
    // Save to database
    await db.files.create({
      filename: file.name,
      url,
      userId: metadata.userId,
      size: file.size,
    })
    
    // Send notifications
    await notificationService.send({
      type: 'upload_complete',
      userId: metadata.userId,
      filename: file.name,
    })
  },
  
  onUploadError: async ({ file, error, metadata }) => {
    console.error(`❌ Upload failed: ${file.name}`, error)
    
    // Log to error tracking service
    await errorService.log({
      operation: 'file_upload',
      error: error.message,
      userId: metadata.userId,
      filename: file.name,
    })
  },
})

Complete Example

// lib/upload.ts
import { createUploadConfig } from 'pushduck/server'

const { storage, s3, config } = 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,
  })
  .defaults({
    maxFileSize: '10MB',
    acl: 'public-read',
  })
  .paths({
    prefix: 'uploads',
    generateKey: (file, metadata) => {
      const userId = metadata.userId || 'anonymous'
      const date = new Date()
      const year = date.getFullYear()
      const month = String(date.getMonth() + 1).padStart(2, '0')
      const day = String(date.getDate()).padStart(2, '0')
      const randomId = Math.random().toString(36).substring(2, 8)
      
      return `${userId}/${year}/${month}/${day}/${randomId}/${file.name}`
    },
  })
  .security({
    allowedOrigins: [
      'http://localhost:3000',
      'https://your-domain.com',
    ],
    rateLimiting: {
      maxUploads: 20,
      windowMs: 60000,
    },
  })
  .hooks({
    onUploadComplete: async ({ file, url, metadata }) => {
      // Save to your database
      await saveFileRecord({
        filename: file.name,
        url,
        userId: metadata.userId,
        uploadedAt: new Date(),
      })
    },
  })
  .build()

export { storage, s3 }

Environment Variables

The configuration automatically reads from environment variables:

# .env.local

# Cloudflare R2
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com
S3_BUCKET_NAME=your-bucket
R2_ACCOUNT_ID=your-account-id

# AWS S3 (alternative)
AWS_REGION=us-east-1
AWS_S3_BUCKET=your-s3-bucket

# DigitalOcean Spaces (alternative)
DO_SPACES_REGION=nyc3
DO_SPACES_BUCKET=your-space
DO_SPACES_ACCESS_KEY_ID=your_key
DO_SPACES_SECRET_ACCESS_KEY=your_secret

# Custom Domains (Optional)
S3_CUSTOM_DOMAIN=https://cdn.yourdomain.com
DO_SPACES_CUSTOM_DOMAIN=https://cdn.yourdomain.com
MINIO_CUSTOM_DOMAIN=https://uploads.yourdomain.com
GCS_CUSTOM_DOMAIN=https://storage.yourdomain.com
CLOUDFLARE_R2_CUSTOM_DOMAIN=https://uploads.yourdomain.com

Custom Domain Configuration

When customDomain is configured, public URLs will use your custom domain instead of the storage provider's default URL. This is useful for:

  • CDN Integration: Use CloudFront, Cloudflare, or other CDNs
  • Branding: Serve files from your own domain
  • Performance: Optimize file delivery with custom caching rules
  • Security: Control access through your own domain

How It Works

// Without custom domain
file.url = "https://bucket.s3.region.amazonaws.com/path/file.jpg"

// With custom domain
file.url = "https://cdn.yourdomain.com/path/file.jpg"

Note: Internal operations (upload, delete) still use the storage provider's endpoints. Only public URLs are affected by the custom domain.

Client Upload Results

After successful upload completion, clients receive file objects with both permanent and temporary URLs:

// Client-side usage
const { uploadFiles } = useUploadRoute('fileUpload', {
  onSuccess: (results) => {
    results.forEach(file => {
      console.log('File:', file.name);
      console.log('Public URL:', file.url);           // Permanent access
      console.log('Download URL:', file.presignedUrl); // Temporary access (1 hour)
      console.log('S3 Key:', file.key);               // Object key/path
    });
  }
});

URL Types

  • url - Permanent URL for public file access, CDN caching, and embedding
  • presignedUrl - Temporary download URL (expires in 1 hour) for secure access
  • key - S3 object key/path for direct storage operations

Use the appropriate URL based on your access requirements:

  • Public files: Use url for permanent access
  • Private files: Use presignedUrl for time-limited access
  • File operations: Use key with storage API

Type Definitions

interface UploadConfig {
  provider: ProviderConfig
  defaults?: {
    maxFileSize?: string | number
    allowedFileTypes?: string[]
    acl?: 'public-read' | 'private'
    metadata?: Record<string, any>
  }
  paths?: {
    prefix?: string
    generateKey?: (
      file: { name: string; type: string },
      metadata: any
    ) => string
  }
  security?: {
    requireAuth?: boolean
    allowedOrigins?: string[]
    rateLimiting?: {
      maxUploads?: number
      windowMs?: number
    }
  }
  hooks?: {
    onUploadStart?: (ctx: { file: any; metadata: any }) => Promise<void> | void
    onUploadComplete?: (ctx: {
      file: any
      url: string          // Permanent file URL
      metadata: any
    }) => Promise<void> | void
    onUploadError?: (ctx: {
      file: any
      error: Error
      metadata: any
    }) => Promise<void> | void
  }
}