uploadthing is a platform for storing files in the cloud. It’s a great alternative to AWS S3 and it’s free for small projects. Here’s how to switch the default storage provider to uploadthing.

1. Swap out the required dependencies

First, uninstall the existing dependencies from the Storage package…

Terminal
pnpm remove @vercel/blob --filter @repo/storage

… and install the new dependencies…

Terminal
pnpm add uploadthing @uploadthing/react --filter @repo/storage

2. Update the environment variables

Next, update the environment variables across the project, for example:

apps/app/.env
// Remove this:
BLOB_READ_WRITE_TOKEN=""

// Add this:
UPLOADTHING_TOKEN=""

Additionally, replace all instances of BLOB_READ_WRITE_TOKEN with UPLOADTHING_TOKEN in the packages/env/index.ts file.

3. Update the existing storage files

Update the index.ts and client.ts to use the new uploadthing packages:

4. Create new files for the storage client

We’ll also need to create a couple of new files for the storage package to handle the Tailwind CSS classes and SSR.

5. Create a file router in your app

Create a new file in your app’s lib directory to define the file router. This file will be used to define the file routes for your app, using your Auth package to get the current user.

apps/app/app/lib/upload.ts
import { currentUser } from '@repo/auth/server';
import { type FileRouter, UploadError, storage } from '@repo/storage';
  
export const router: FileRouter = {
  imageUploader: storage({
    image: {
      maxFileSize: '4MB',
      maxFileCount: 1,
    },
  })
    .middleware(async () => {
      const user = await currentUser();

      if (!user) {
        throw new UploadError('Unauthorized');
      }

      return { userId: user.id };
    })
    .onUploadComplete(({ metadata, file }) => ({ uploadedBy: metadata.userId }),
};

6. Create a route handler

Create a new route handler in your app’s api directory to handle the file routes.

apps/app/app/api/upload/route.ts
import { router } from '@/app/lib/upload';
import { createRouteHandler } from '@repo/storage';

export const { GET, POST } = createRouteHandler({ router });

7. Update your root layout

Update your root layout to include the StorageSSRPlugin. This will add SSR hydration and avoid a loading state on your upload button.

apps/app/app/layout.tsx
import '@repo/design-system/styles/globals.css';
import { DesignSystemProvider } from '@repo/design-system';
import { fonts } from '@repo/design-system/lib/fonts';
import { extractRouterConfig } from '@repo/storage';
import { StorageSSRPlugin } from '@repo/storage/ssr';
import type { ReactNode } from 'react';
import { router } from './lib/upload';

type RootLayoutProperties = {
  readonly children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProperties) => (
  <html lang="en" className={fonts} suppressHydrationWarning>
    <body>
      <StorageSSRPlugin routerConfig={extractRouterConfig(router)} />
      <DesignSystemProvider>{children}</DesignSystemProvider>
    </body>
  </html>
);

export default RootLayout;

8. Update your app’s Tailwind config

Update your app’s Tailwind config to include the withStorage function. This will add the necessary classes to your upload button.

apps/app/tailwind.config.ts
import { withUt } from '@repo/storage/tailwind';
import { config } from '@repo/tailwind-config/config';

export default withUt(config) as typeof config;

9. Create your upload button

Create a new component for your upload button. This will use the generateUploadButton function to create a button that will upload files to the imageUploader endpoint.

apps/app/app/(authenticated)/components/upload-button.tsx
'use client';

import type { router } from '@/app/lib/upload';
import { generateUploadButton } from '@repo/storage/client';
import { toast } from 'sonner';

const UploadButton = generateUploadButton<typeof router>();

export const UploadForm = () => (
  <UploadButton
    endpoint="imageUploader"
    onClientUploadComplete={(res) => {
      // Do something with the response
      console.log('Files: ', res);
      toast.success('Upload Completed');
    }}
    onUploadError={(error: Error) => {
      toast.error(`ERROR! ${error.message}`);
    }}
  />
);

Now you can import this component into your app and use it as a regular component.

10. Advanced configuration

uploadthing is a powerful platform that offers a lot of advanced configuration options. You can learn more about them in the uploadthing documentation.

File Routes

Learn how to define file routes

Security

How to protect different parts of the UploadThing flow.