This guide will walk you through recreating the Droply project, a file storage application built with Next.js, Clerk, Neon PostgreSQL, Drizzle ORM, and HeroUI.
Before starting, make sure you have the following:
- Node.js 18+ and npm
- A Clerk account for authentication
- A Neon PostgreSQL database
- An ImageKit account for file storage
- Create a new Next.js project:
npx create-next-app@latest droply
cd droply- When prompted, choose the following options:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes
- App Router: Yes
- Import alias: Yes (default: @/*)
Install the required dependencies:
npm install @clerk/nextjs @heroui/avatar @heroui/badge @heroui/button @heroui/card @heroui/code @heroui/divider @heroui/dropdown @heroui/input @heroui/kbd @heroui/link @heroui/listbox @heroui/modal @heroui/navbar @heroui/progress @heroui/snippet @heroui/spinner @heroui/switch @heroui/system @heroui/table @heroui/tabs @heroui/theme @heroui/toast @heroui/tooltip @hookform/resolvers @neondatabase/serverless @react-aria/ssr @react-aria/visually-hidden axios clsx date-fns dotenv drizzle-orm framer-motion imagekit imagekitio-next intl-messageformat lucide-react next-themes react-hook-form uuidInstall dev dependencies:
npm install -D @next/eslint-plugin-next @react-types/shared @tailwindcss/postcss @types/node @types/react @types/react-dom @types/uuid @typescript-eslint/eslint-plugin @typescript-eslint/parser autoprefixer drizzle-kit eslint eslint-config-next eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-node eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-unused-imports postcss prettier tailwind-variants tailwindcss tsx typescript- Create a
.env.examplefile in the root directory:
# Clerk Authentication
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_publishable_key
CLERK_SECRET_KEY=your_clerk_secret_key
# ImageKit
NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY=your_imagekit_public_key
IMAGEKIT_PRIVATE_KEY=your_imagekit_private_key
NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT=your_imagekit_url_endpoint
# Clerk URLs
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard
# Fallback URLs
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
# App URLs
NEXT_PUBLIC_APP_URL=http://localhost:3000
# Database - Neon PostgreSQL
DATABASE_URL=your_neon_database_url
- Create a
.env.localfile with your actual credentials.
Update tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./node_modules/@heroui/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};- Create
drizzle.config.tsin the root directory:
import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";
dotenv.config();
export default {
schema: "./lib/db/schema.ts",
out: "./drizzle",
driver: "pg",
dbCredentials: {
connectionString: process.env.DATABASE_URL || "",
},
verbose: true,
strict: true,
} satisfies Config;- Create database schema in
lib/db/schema.ts:
/**
* Database Schema for Droply
*
* This file defines the database structure for our Droply application.
* We're using Drizzle ORM with PostgreSQL (via Neon) for our database.
*/
import {
pgTable,
text,
timestamp,
uuid,
integer,
boolean,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
/**
* Files Table
*
* This table stores all files and folders in our Droply.
* - Both files and folders are stored in the same table
* - Folders are identified by the isFolder flag
* - Files/folders can be nested using the parentId (creating a tree structure)
*/
export const files = pgTable("files", {
// Unique identifier for each file/folder
id: uuid("id").defaultRandom().primaryKey(),
// Basic file/folder information
name: text("name").notNull(),
path: text("path").notNull(), // Full path to the file/folder
size: integer("size").notNull(), // Size in bytes (0 for folders)
type: text("type").notNull(), // MIME type for files, "folder" for folders
// Storage information
fileUrl: text("file_url").notNull(), // URL to access the file
thumbnailUrl: text("thumbnail_url"), // Optional thumbnail for images/documents
// Ownership and hierarchy
userId: text("user_id").notNull(), // Owner of the file/folder
parentId: uuid("parent_id"), // Parent folder ID (null for root items)
// File/folder flags
isFolder: boolean("is_folder").default(false).notNull(), // Whether this is a folder
isStarred: boolean("is_starred").default(false).notNull(), // Starred/favorite items
isTrash: boolean("is_trash").default(false).notNull(), // Items in trash
// Timestamps
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
/**
* File Relations
*
* This defines the relationships between records in our files table:
* 1. parent - Each file/folder can have one parent folder
* 2. children - Each folder can have many child files/folders
*
* This creates a hierarchical file structure similar to a real filesystem.
*/
export const filesRelations = relations(files, ({ one, many }) => ({
// Relationship to parent folder
parent: one(files, {
fields: [files.parentId], // The foreign key in this table
references: [files.id], // The primary key in the parent table
}),
// Relationship to child files/folders
children: many(files),
}));
/**
* Type Definitions
*
* These types help with TypeScript integration:
* - File: Type for retrieving file data from the database
* - NewFile: Type for inserting new file data into the database
*/
export type File = typeof files.$inferSelect;
export type NewFile = typeof files.$inferInsert;- Create database connection in
lib/db/index.ts:
import { neon, neonConfig } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
// Configure Neon to use WebSockets
neonConfig.fetchConnectionCache = true;
// Create a SQL client with the connection string
const sql = neon(process.env.DATABASE_URL!);
// Create a Drizzle client with the SQL client and schema
export const db = drizzle(sql, { schema });- Create migration script in
lib/db/migrate.ts:
import { migrate } from "drizzle-orm/neon-http/migrator";
import { db } from "./index";
// This script will run all migrations in the drizzle directory
async function main() {
console.log("Running migrations...");
try {
await migrate(db, { migrationsFolder: "drizzle" });
console.log("Migrations completed successfully");
} catch (error) {
console.error("Error running migrations:", error);
process.exit(1);
}
process.exit(0);
}
main();- Add utility functions in
lib/utils.ts:
export function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}- Create a middleware.ts file in the root directory:
import { authMiddleware } from "@clerk/nextjs";
export default authMiddleware({
// Public routes that don't require authentication
publicRoutes: [
"/",
"/sign-in(.*)",
"/sign-up(.*)",
"/api/imagekit-auth",
],
// Routes that can be accessed by authenticated users or via an API key
ignoredRoutes: [
"/api/webhooks(.*)",
],
});
export const config = {
// Protects all routes, including api/trpc.
// See https://clerk.com/docs/references/nextjs/auth-middleware
matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
};- Create
app/providers.tsx:
"use client";
import { ClerkProvider } from "@clerk/nextjs";
import { ThemeProvider } from "next-themes";
import { ToastProvider } from "@heroui/toast";
import { SSRProvider } from "@react-aria/ssr";
import { VisuallyHidden } from "@react-aria/visually-hidden";
import { useRouter } from "next/navigation";
interface ProvidersProps {
children: React.ReactNode;
}
export default function Providers({ children }: ProvidersProps) {
const router = useRouter();
return (
<SSRProvider>
<ClerkProvider
appearance={{
variables: {
colorPrimary: "#0070f3",
},
}}
navigate={(to) => router.push(to)}
>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem={true}
themes={["light", "dark"]}
>
<ToastProvider>
<VisuallyHidden>
<h1>Droply - Simple File Storage</h1>
</VisuallyHidden>
{children}
</ToastProvider>
</ThemeProvider>
</ClerkProvider>
</SSRProvider>
);
}- Create
app/layout.tsx:
import "./globals.css";
import type { Metadata } from "next";
import Providers from "./providers";
export const metadata: Metadata = {
title: "Droply - Simple File Storage",
description: "A simple file storage application",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Create the following components in the components directory:
- Navbar.tsx
- DashboardContent.tsx
- FileUploadForm.tsx
- FileList.tsx
- FileIcon.tsx
- FileActions.tsx
- FileActionButtons.tsx
- FileEmptyState.tsx
- FileLoadingState.tsx
- FileTabs.tsx
- FolderNavigation.tsx
- UserProfile.tsx
- ConfirmationModal.tsx (in components/ui directory)
Create the following API routes:
app/api/files/upload/route.ts- For file uploadsapp/api/files/route.ts- For fetching filesapp/api/files/[id]/star/route.ts- For starring/unstarring filesapp/api/files/[id]/trash/route.ts- For moving files to trashapp/api/files/[id]/delete/route.ts- For permanently deleting filesapp/api/folders/create/route.ts- For creating foldersapp/api/imagekit-auth/route.ts- For ImageKit authentication
- Create
app/page.tsx- Landing page - Create
app/dashboard/page.tsx- Dashboard page - Create
app/sign-in/[[...sign-in]]/page.tsx- Sign in page - Create
app/sign-up/[[...sign-up]]/page.tsx- Sign up page - Create
app/error.tsx- Error page
Run the database migrations:
npm run db:generate
npm run db:pushStart the development server:
npm run devVisit http://localhost:3000 to see your application.
When you're ready to deploy:
npm run build
npm start- Make sure to set up your Clerk, Neon, and ImageKit accounts properly
- Update the environment variables with your actual credentials
- The application uses HeroUI components for the UI
- File uploads are handled by ImageKit
- Authentication is handled by Clerk
- Database operations are handled by Drizzle ORM with Neon PostgreSQL