Pushduck
Pushduck// S3 uploads for any framework

SvelteKit

Fast, modern file uploads with SvelteKit using Web Standards - no adapter needed!

🚧 Client-Side In Development: SvelteKit server-side integration is fully functional with Web Standards APIs. However, SvelteKit-specific client-side components and hooks are still in development. You can use the standard pushduck client APIs for now.

Using pushduck with SvelteKit

SvelteKit is the official application framework for Svelte. It uses Web Standards APIs and provides excellent performance with minimal JavaScript. Since SvelteKit uses standard Request/Response objects, pushduck handlers work directly without any adapters!

Web Standards Native: SvelteKit server endpoints use Web Standard Request/Response objects, making pushduck integration straightforward with zero overhead.

Quick Setup

Install dependencies

npm install pushduck

Configure upload router

src/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

src/routes/api/upload/[...path]/+server.ts
import type { RequestHandler } from './$types';
import { uploadRouter } from '$lib/upload';

// Direct usage - no adapter needed!
export const GET: RequestHandler = async ({ request }) => {
  return uploadRouter.handler(request);
};

export const POST: RequestHandler = async ({ request }) => {
  return uploadRouter.handler(request);
};

Basic Integration

Simple Upload Route

src/routes/api/upload/[...path]/+server.ts
import type { RequestHandler } from './$types';
import { uploadRouter } from '$lib/upload';

// Method 1: Combined handler (recommended)
export const GET: RequestHandler = async ({ request }) => {
  return uploadRouter.handler(request);
};

export const POST: RequestHandler = async ({ request }) => {
  return uploadRouter.handler(request);
};

// Method 2: Separate handlers (if you need method-specific logic)
// export const { GET, POST } = uploadRouter.handlers;

With SvelteKit Hooks

src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';

const corsHandler: Handle = async ({ event, resolve }) => {
  if (event.url.pathname.startsWith('/api/upload')) {
    if (event.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 resolve(event);
  
  if (event.url.pathname.startsWith('/api/upload')) {
    response.headers.set('Access-Control-Allow-Origin', '*');
  }
  
  return response;
};

export const handle = sequence(corsHandler);

Advanced Configuration

Authentication with SvelteKit

src/lib/upload.ts
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({
  // Private uploads with session 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;

Client-Side Usage

SvelteKit uses the Svelte binding, pushduck/svelte. createUploadRoute returns a readable store plus the imperative controls, so $upload auto-subscribes in both Svelte 4 and Svelte 5.

pushduck/svelte requires Svelte 4+. It implements the store contract structurally and does not import svelte/store, so it adds nothing to your bundle beyond the upload logic itself.

Upload Component

src/lib/components/FileUpload.svelte
<script lang="ts">
  import { createUploadRoute } from "pushduck/svelte";
  import type { AppUploadRouter } from "$lib/upload";

  const upload = createUploadRoute<AppUploadRouter>("imageUpload", {
    endpoint: "/api/upload",
    onSuccess: (files) => console.log("Files uploaded:", files),
    onError: (error) => console.error("Upload failed:", error),
  });

  function onSelect(event: Event) {
    const input = event.target as HTMLInputElement;
    upload.uploadFiles([...(input.files ?? [])]);
  }
</script>

<div class="space-y-4">
  <input
    type="file"
    multiple
    accept="image/*"
    disabled={$upload.isUploading}
    on:change={onSelect}
  />

  {#if $upload.isUploading}
    <progress value={$upload.progress} max="100"></progress>
  {/if}

  {#each $upload.files as file (file.id)}
    <div class="flex gap-2 items-center">
      <span>{file.name}</span>
      <span>{file.status} ({file.progress}%)</span>
      {#if file.status === "uploading"}
        <button on:click={() => upload.cancel(file.id)}>Cancel</button>
      {/if}
      {#if file.url}
        <a href={file.url} target="_blank" rel="noreferrer">View</a>
      {/if}
    </div>
  {/each}

  {#each $upload.errors as error}
    <p class="text-red-600">{error}</p>
  {/each}

  {#if $upload.files.length}
    <button on:click={upload.reset}>Clear</button>
  {/if}
</div>

Using in Routes

src/routes/+page.svelte
<script lang="ts">
  import FileUpload from "$lib/components/FileUpload.svelte";
</script>

<svelte:head>
  <title>File Upload Demo</title>
</svelte:head>

<div class="container mx-auto px-4 py-8">
  <h1 class="text-3xl font-bold mb-8">File Upload Demo</h1>
  <FileUpload />
</div>

Svelte 5 Runes

Stores remain first-class under runes, so the same code works unchanged. Derive from the store where you want a local reactive value:

src/lib/components/RunesUpload.svelte
<script lang="ts">
  import { createUploadRoute } from "pushduck/svelte";

  const upload = createUploadRoute("imageUpload", { endpoint: "/api/upload" });

  let percent = $derived(Math.round($upload.progress));
  let done = $derived($upload.files.filter((f) => f.status === "success"));
</script>

<p>{percent}% — {done.length} finished</p>

Passing Client Metadata

src/lib/components/AlbumUpload.svelte
<script lang="ts">
  import { createUploadRoute } from "pushduck/svelte";

  let albumId = "vacation-2026";
  const upload = createUploadRoute("imageUpload", { endpoint: "/api/upload" });

  function onSelect(event: Event) {
    const input = event.target as HTMLInputElement;
    upload.uploadFiles([...(input.files ?? [])], {
      albumId,
      visibility: "private",
    });
  }
</script>

Client metadata is untrusted user input. Validate and sanitize it in your server middleware, and never trust client-provided identity claims such as userId or role — derive those from the authenticated session instead.

File Management

Server-Side File Listing

src/routes/files/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/database';

export const load: PageServerLoad = async ({ locals }) => {
  const files = await db.file.findMany({
    where: { userId: locals.user?.id },
    orderBy: { createdAt: 'desc' },
  });
  
  return {
    files: files.map(file => ({
      id: file.id,
      name: file.name,
      url: file.url,
      size: file.size,
      uploadedAt: file.createdAt,
    })),
  };
};
src/routes/files/+page.svelte
<script lang="ts">
  import type { PageData } from './$types';
  import FileUpload from '$lib/components/FileUpload.svelte';
  
  export let data: PageData;
  
  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];
  }
</script>

<svelte:head>
  <title>My Files</title>
</svelte:head>

<main class="container mx-auto px-4 py-8">
  <h1 class="text-3xl font-bold mb-8">My Files</h1>
  
  <div class="mb-8">
    <FileUpload />
  </div>
  
  <div>
    <h2 class="text-2xl font-semibold mb-4">Uploaded Files</h2>
    
    {#if data.files.length === 0}
      <p class="text-gray-500">No files uploaded yet.</p>
    {:else}
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {#each data.files as file}
          <div class="border rounded-lg p-4 hover:shadow-md transition-shadow">
            <h3 class="font-medium truncate" title={file.name}>
              {file.name}
            </h3>
            <p class="text-sm text-gray-500">
              {formatFileSize(file.size)}
            </p>
            <p class="text-sm text-gray-500">
              {new Date(file.uploadedAt).toLocaleDateString()}
            </p>
            <a
              href={file.url}
              target="_blank"
              rel="noopener noreferrer"
              class="text-blue-500 hover:underline text-sm mt-2 inline-block"
            >
              View File
            </a>
          </div>
        {/each}
      </div>
    {/if}
  </div>
</main>

Deployment Options

svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  
  kit: {
    adapter: adapter({
      runtime: 'nodejs18.x',
      regions: ['iad1'],
    }),
  }
};

export default config;
svelte.config.js
import adapter from '@sveltejs/adapter-netlify';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter({
      edge: false,
      split: false
    }),
  }
};

export default config;
svelte.config.js
import adapter from '@sveltejs/adapter-node';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter({
      out: 'build'
    }),
  }
};

export default config;
svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter({
      routes: {
        include: ['/*'],
        exclude: ['<build>']
      }
    }),
  }
};

export default config;

Environment Variables

.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

# SvelteKit
PUBLIC_UPLOAD_ENDPOINT=http://localhost:5173/api/upload

Performance Benefits

Minimal JavaScript

Svelte compiles to vanilla JS with small bundle sizes

Web Standards

No adapter overhead - direct Request/Response usage

Fast Hydration

Selective hydration of interactive components

Edge Ready

Works on edge runtimes and CDNs

Real-Time Upload Progress

src/lib/components/AdvancedUpload.svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { writable } from 'svelte/store';
  
  const uploadProgress = writable(0);
  const isUploading = writable(false);
  
  let fileInput: HTMLInputElement;
  
  async function handleFileUpload(event: Event) {
    const target = event.target as HTMLInputElement;
    const files = target.files;
    
    if (!files || files.length === 0) return;
    
    isUploading.set(true);
    uploadProgress.set(0);
    
    try {
      // Simulate upload progress
      for (let i = 0; i <= 100; i += 10) {
        uploadProgress.set(i);
        await new Promise(resolve => setTimeout(resolve, 100));
      }
      
      alert('Upload completed!');
    } catch (error) {
      console.error('Upload failed:', error);
      alert('Upload failed!');
    } finally {
      isUploading.set(false);
      uploadProgress.set(0);
    }
  }
</script>

<div class="upload-container">
  <input
    bind:this={fileInput}
    type="file"
    multiple
    on:change={handleFileUpload}
    disabled={$isUploading}
    class="file-input"
  />
  
  {#if $isUploading}
    <div class="progress-container">
      <div class="progress-bar" style="width: {$uploadProgress}%"></div>
    </div>
    <p class="progress-text">{$uploadProgress}% uploaded</p>
  {/if}
</div>

<style>
  .upload-container {
    max-width: 400px;
    margin: 0 auto;
  }
  
  .file-input {
    width: 100%;
    padding: 12px;
    border: 2px dashed #ccc;
    border-radius: 8px;
    cursor: pointer;
  }
  
  .file-input:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
  
  .progress-container {
    width: 100%;
    height: 8px;
    background-color: #f0f0f0;
    border-radius: 4px;
    margin-top: 12px;
    overflow: hidden;
  }
  
  .progress-bar {
    height: 100%;
    background-color: #4CAF50;
    transition: width 0.3s ease;
  }
  
  .progress-text {
    text-align: center;
    margin-top: 8px;
    font-size: 14px;
    color: #666;
  }
</style>

Troubleshooting

Common Issues

  1. Route not found: Ensure your route is src/routes/api/upload/[...path]/+server.ts
  2. Build errors: Check that pushduck is properly installed
  3. CORS issues: SvelteKit handles CORS automatically for same-origin requests

Debug Mode

Enable debug logging:

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 {};
});

SvelteKit provides an excellent foundation for building fast, modern web applications with pushduck, combining the power of Svelte's reactive framework with Web Standards APIs.