UploadKit
Core Concepts

Bring Your Own Storage (BYOS)

Use your own S3 or R2 bucket with zero frontend changes. No vendor lock-in.

What is BYOS?

BYOS (Bring Your Own Storage) lets you use the UploadKit SDK and React components while storing files in your own S3-compatible bucket. No vendor lock-in.

UploadKit Cloud vs self-hosted BYOS

  • UploadKit Cloud (this dashboard, uk_live_* / uk_test_* keys): files always land on UploadKit's managed R2 + CDN. There is no BYOS toggle in project settings.
  • Self-hosted BYOS (open-source path): you run createUploadKitHandler in your app with a storage adapter. Same components on the frontend; credentials live in your server environment only.

Pick one path per deployment — they are not mixed inside a single Cloud project.

In managed Cloud mode, files go to UploadKit's infrastructure. In BYOS mode, they go to your bucket — but the developer experience is identical. The same UploadButton, UploadDropzone, and useUploadKit work in both modes. Your frontend code never changes.

Setup

Configure BYOS in createUploadKitHandler by passing a storage object with your bucket credentials:

app/api/uploadkit/route.ts
import { createUploadKitHandler } from '@uploadkitdev/next';
import type { FileRouter } from '@uploadkitdev/next';

const router = {
  imageUploader: {
    maxFileSize: '4MB',
    allowedTypes: ['image/*'],
    onUploadComplete: async ({ file }) => {
      console.log('Stored in your R2 bucket:', file.url);
    },
  },
} satisfies FileRouter;

export const { GET, POST } = createUploadKitHandler({
  router,
  storage: {
    endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
    region: 'auto',
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
    bucket: process.env.R2_BUCKET_NAME!,
  },
});
.env.local
R2_ACCOUNT_ID=your_cloudflare_account_id
R2_ACCESS_KEY_ID=your_r2_access_key_id
R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
R2_BUCKET_NAME=your-bucket-name

Get your R2 credentials from the Cloudflare dashboard → R2 → Manage API Tokens.

app/api/uploadkit/route.ts
import { createUploadKitHandler } from '@uploadkitdev/next';
import type { FileRouter } from '@uploadkitdev/next';

const router = {
  documentUploader: {
    maxFileSize: '16MB',
    allowedTypes: ['application/pdf'],
  },
} satisfies FileRouter;

export const { GET, POST } = createUploadKitHandler({
  router,
  storage: {
    region: process.env.AWS_REGION!,
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    bucket: process.env.AWS_S3_BUCKET!,
  },
});
.env.local
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
AWS_S3_BUCKET=my-upload-bucket

Create an IAM user with s3:PutObject, s3:GetObject, s3:DeleteObject, and s3:AbortMultipartUpload permissions on your bucket.

app/api/uploadkit/route.ts
import { createUploadKitHandler } from '@uploadkitdev/next';
import type { FileRouter } from '@uploadkitdev/next';

const router = {
  fileUploader: {
    maxFileSize: '50MB',
    allowedTypes: ['*/*'],
  },
} satisfies FileRouter;

export const { GET, POST } = createUploadKitHandler({
  router,
  storage: {
    endpoint: process.env.MINIO_ENDPOINT!, // e.g. "http://localhost:9000"
    region: 'us-east-1', // MinIO requires a region value, even though it's self-hosted
    accessKeyId: process.env.MINIO_ACCESS_KEY!,
    secretAccessKey: process.env.MINIO_SECRET_KEY!,
    bucket: process.env.MINIO_BUCKET!,
  },
});
.env.local
MINIO_ENDPOINT=http://localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=uploads

Supported providers

Any S3-compatible object storage works with BYOS:

Providerendpointregion
Cloudflare R2https://<ACCOUNT_ID>.r2.cloudflarestorage.comauto
AWS S3(omit — uses AWS default)us-east-1 (or your region)
DigitalOcean Spaceshttps://<region>.digitaloceanspaces.com<region> (e.g. nyc3)
MinIOhttp://your-minio-host:9000any string (e.g. us-east-1)
Wasabihttps://s3.wasabisys.comus-east-1
Backblaze B2https://s3.us-west-004.backblazeb2.comus-west-004

The endpoint field is only required when using non-AWS providers. For standard AWS S3, omit it and the SDK uses the default regional endpoint.

Zero frontend changes

The storage configuration lives entirely in your server-side file router handler. Your React components and the REST API surface are completely unchanged:

// This code is identical in managed mode and BYOS mode
<UploadDropzone
  route="imageUploader"
  onUploadComplete={(files) => {
    // files[0].url points to YOUR bucket, not UploadKit's managed storage
    saveToDatabase(files[0].url);
  }}
/>

The only difference is where files end up — in your bucket, using your credentials.

Security model

BYOS credentials never touch the browser. Here's why that's safe:

  1. Your storage credentials (accessKeyId, secretAccessKey) live in server-side environment variables
  2. When a client requests an upload, your server uses those credentials to generate a presigned PUT URL
  3. The presigned URL is time-limited (1 hour), scoped to a single object key, and signed by your credentials
  4. The client uses the presigned URL to upload — it never sees your credentials
Browser                     Your server                     Your S3 bucket
  │                              │                                │
  │  "Upload imageUploader" ────►│                                │
  │                              │ Uses your credentials         │
  │                              │ to generate presigned URL ───►│
  │  ◄── presigned URL ──────────│                                │
  │                              │                                │
  │  PUT file ───────────────────┼───────────────────────────────►│
  │  (using presigned URL)       │                                │
  │                              │                                │
  │  "Done" ─────────────────────►│                               │

Never pass accessKeyId or secretAccessKey to the client. The storage config belongs in server-side code only — never in a client component or a NEXT_PUBLIC_ environment variable.

Managed vs BYOS: when to use which

Managed (default)BYOS
Setup timeZero — works out of the box~10 minutes bucket config
Storage locationUploadKit's Cloudflare R2Your own bucket
Credential managementUploadKit handles itYou manage IAM/API keys
Data sovereigntyShared infrastructureFull control
Compliance (HIPAA, GDPR)Check UploadKit DPAYour existing compliance
CDNIncluded (Cloudflare)Configure separately
CostIncluded in UploadKit tierYour provider's storage rates
Vendor lock-inTied to UploadKit storageSwitch providers freely

Use managed when: you want zero infrastructure overhead, you're comfortable with shared storage, and you're on the Free or Pro tier.

Use BYOS when: you need data sovereignty, have existing storage infrastructure, are subject to compliance requirements, or want to avoid UploadKit storage costs for high-volume use.

See the Security page for details on how UploadKit protects your storage credentials and validates uploads in BYOS mode.

On this page