A full-stack YouTube clone built with Next.js 15, tRPC, and modern web technologies
Quick Start • Environment • Architecture • Code Style
- 📑 Table of Contents
- 📁 Project Structure
- 🚀 Quick Start
- ⚙️ Environment Variables
- 🏛️ Architecture
- 💻 Code Style & Patterns
- 🛠️ Tech Stack
- 📄 License
newtube-clone/
├── 📁 app/ # Next.js App Router
│ ├── 📁 (auth)/ # Authentication routes group
│ │ ├── 📁 sign-in/ # Sign-in page
│ │ └── 📁 sign-up/ # Sign-up page
│ ├── 📁 (home)/ # Main application routes
│ │ ├── 📁 feed/ # Video feed pages
│ │ │ ├── 📁 subscriptions/ # Subscribed channels
│ │ │ └── 📁 trending/ # Trending videos
│ │ ├── 📁 playlists/ # Playlist management
│ │ ├── 📁 search/ # Search functionality
│ │ ├── 📁 users/[userId]/ # User channel pages
│ │ └── 📁 videos/[videoId]/ # Video watch pages
│ ├── 📁 (studio)/ # Creator studio
│ │ └── 📁 studio/ # Video management & editing
│ └── 📁 api/ # API routes
│ ├── 📁 trpc/ # tRPC handlers
│ ├── 📁 uploadthing/ # File upload handlers
│ ├── 📁 users/ # User webhooks
│ └── 📁 videos/ # Video webhooks & workflows
│
├── 📁 components/ # Shared UI components
│ └── 📁 ui/ # shadcn/ui components
│
├── 📁 db/ # Database layer
│ ├── 📄 db.ts # Database connection
│ └── 📄 schema.ts # Drizzle ORM schema
│
├── 📁 hooks/ # Custom React hooks
│ ├── 📄 use-mobile.tsx # Mobile detection hook
│ ├── 📄 use-intersection-observer.tsx
�� └── 📄 use-subscription.ts # Subscription state hook
│
├── 📁 lib/ # Core libraries & utilities
│ ├── 📄 auth.ts # Authentication helpers
│ ├── 📄 mux.ts # Mux video SDK
│ ├── 📄 redis.ts # Upstash Redis client
│ ├── 📄 ratelimit.ts # Rate limiting
│ ├── 📄 uploadthing.ts # File upload utilities
│ ├── 📄 workflow.ts # QStash workflow client
│ └── 📄 utils.ts # Utility functions
│
├── 📁 moubles/ # Feature modules (domain-driven)
│ ├── 📁 auth/ # Authentication features
│ ├── 📁 categories/ # Video categories
│ ├── 📁 comments/ # Comment system
│ ├── 📁 home/ # Home page features
│ ├── 📁 playlists/ # Playlist management
│ ├── 📁 search/ # Search functionality
│ ├── 📁 studio/ # Creator studio
│ ├── 📁 subscriptions/ # Subscription system
│ ├── 📁 suggestions/ # Video suggestions
│ ├── 📁 users/ # User profiles
│ ├── 📁 video-reactions/ # Likes/dislikes
│ ├── 📁 video-views/ # View tracking
│ └── 📁 videos/ # Video core features
│
├── 📁 trpc/ # tRPC configuration
│ ├── 📄 client.tsx # Client-side tRPC
│ ├── 📄 server.tsx # Server-side tRPC
│ ├── 📄 init.ts # tRPC initialization & middleware
│ ├── 📄 query-client.ts # TanStack Query config
│ └── 📁 router/ # API routers
│
├── 📁 migrations/ # Database migrations
├── 📁 public/ # Static assets
├── 📁 logo/ # Architecture diagrams
└── 📁 scripts/ # Utility scripts
- Node.js >= 18.x
- Bun (recommended) or npm/pnpm/yarn
- PostgreSQL database (Neon, Supabase, or local)
-
Clone the repository
git clone https://github.com/your-username/newtube-clone.git cd newtube-clone -
Install dependencies
bun install # or npm install -
Set up environment variables
cp .env.example .env.local
Then fill in your environment variables (see Environment Variables)
-
Push database schema
bun run db:push # or npm run db:push -
Seed categories (optional)
bun run scripts/seed-categories.ts
-
Start development server
bun run dev # or npm run dev -
Open your browser Navigate to http://localhost:3000
Create a .env.local file in the root directory with the following variables:
# ==============================================
# Database
# ==============================================
DATABASE_URL="postgresql://user:password@host:5432/database?sslmode=require"
# ==============================================
# Clerk Authentication
# ==============================================
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_xxxxx"
CLERK_SECRET_KEY="sk_test_xxxxx"
CLERK_WEBHOOK_SIGNING_SECRET="whsec_xxxxx"
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"
# ==============================================
# Mux Video Processing
# ==============================================
MUX_TOKEN_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
MUX_TOKEN_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
MUX_WEBHOOK_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ==============================================
# UploadThing (File Uploads)
# ==============================================
UPLOADTHING_TOKEN="sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ==============================================
# Upstash Redis & QStash
# ==============================================
UPSTASH_REDIS_REST_URL="https://xxxx-xxxx-xxxx-xxxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
QSTASH_URL="https://qstash.upstash.io"
QSTASH_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
QSTASH_WORKFLOW_URL="https://your-domain.com" # Your production domain or ngrok URL
# ==============================================
# AI Services (Optional)
# ==============================================
OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# or
ZHIPU_API_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# ==============================================
# Ngrok (Development only)
# ==============================================
NGROK_AUTHTOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"Why Neon? Serverless PostgreSQL with generous free tier.
- Go to Neon Console
- Create a new project
- Copy the connection string from Dashboard → Connection Details
- Paste as
DATABASE_URL
Format: postgresql://[user]:[password]@[endpoint].[region].neon.tech/[database]?sslmode=require
Why Clerk? Complete authentication solution with webhooks.
-
Go to Clerk Dashboard
-
Create a new application
-
Get API Keys:
- Navigate to API Keys
- Copy
Publishable Key→NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY - Copy
Secret Key→CLERK_SECRET_KEY
-
Configure Sign-in/Sign-up URLs:
- Go to Paths in sidebar
- Set Sign-in URL:
/sign-in - Set Sign-up URL:
/sign-up
-
Set up Webhook:
- Go to Webhooks → Add Endpoint
- Endpoint URL:
https://your-domain.com/api/users/webhook - Subscribe to events:
user.createduser.updateduser.deleted
- Copy the Signing Secret →
CLERK_WEBHOOK_SIGNING_SECRET
Why Mux? Professional video streaming with HLS, auto-generated subtitles.
-
Go to Mux Dashboard
-
Create a new environment (or use the Development environment)
-
Get API Tokens:
- Navigate to Settings → API Access Tokens
- Create a new token with Full Access
- Copy
Token ID→MUX_TOKEN_ID - Copy
Token Secret→MUX_TOKEN_SECRET
-
Set up Webhook:
- Go to Settings → Webhooks
- Add a new webhook:
https://your-domain.com/api/videos/webhook - Select events:
video.upload.cancelledvideo.asset.createdvideo.asset.readyvideo.asset.erroredvideo.track.createdvideo.track.ready
- Copy the webhook secret →
MUX_WEBHOOK_SECRET
Why UploadThing? Simple file uploads for thumbnails and banners.
- Go to UploadThing Dashboard
- Create a new app
- Copy the API key from API Keys →
UPLOADTHING_TOKEN
Why Upstash? Serverless Redis for caching and task queues.
- Go to Upstash Console
- Create a new Redis database
- Copy:
UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN
- In Upstash Console, go to QStash
- Create a new QStash instance
- Copy:
QSTASH_URLQSTASH_TOKEN
- Set
QSTASH_WORKFLOW_URLto your domain (or ngrok URL for development)
Used for auto-generating video titles, descriptions, and thumbnails.
- Go to OpenAI API Keys
- Create a new API key
- Copy →
OPENAI_API_KEY
- Go to ZhiPu AI
- Get API key from console
- Copy →
ZHIPU_API_KEY
Required for local webhook testing
- Go to Ngrok Dashboard
- Get your auth token from Your Authtoken
- Copy →
NGROK_AUTHTOKEN - Run with
bun run dev:allto start both Next.js and ngrok
The application follows a three-tier architecture with modern serverless patterns:
Architecture Layers:
| Layer | Technology | Purpose |
|---|---|---|
| Presentation | Next.js 15 (App Router) | SSR/SSG, routing, UI components |
| API | tRPC | Type-safe RPC layer with middleware |
| Data | PostgreSQL + Drizzle ORM | Persistent storage with type-safe queries |
| External Services | Clerk, Mux, UploadThing, Upstash | Authentication, video, uploads, caching |
Key Design Decisions:
- Server Components by Default - Most components are server-rendered for optimal performance and SEO
- tRPC for Type Safety - End-to-end type safety from database to frontend
- Feature-based Module Structure - Code organized by domain (moubles/)
- Edge-compatible Runtime - Optimized for Vercel Edge and serverless deployments
Request Flow:
Client Component
│
│ trpc.videos.getMany.useQuery()
▼
┌─────────────────────────────────────────────────────────┐
│ tRPC Client (type-safe) │
│ - Automatic serialization (superjson) │
│ - Request batching │
└─────────────────────────────────────────────────────────┘
│
│ HTTP POST /api/trpc/videos.getMany
▼
┌─────────────────────────────────────────────────────────┐
│ protectedProcedure Middleware │
│ 1. Verify Clerk JWT │
│ 2. Get dbUserId (JWT metadata → Redis → Database) │
│ 3. Rate limit check (Upstash) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Route Handler (videos.getMany) │
│ - Input validation (Zod) │
│ - Business logic │
│ - Database query (Drizzle ORM) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Response │
│ - Type-safe JSON response │
│ - Cached by TanStack Query │
└─────────────────────────────────────────────────────────┘
Authentication Flow:
┌─────────────────────────────────────────────────────────────────┐
│ protectedProcedure │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Check JWT for clerkUserId │
│ if (!clerkUserId) → 401 UNAUTHORIZED │
│ │
│ 2. Check JWT publicMetadata for dbUserId (optimized path) │
│ if (dbUserId) → Skip database query │
│ │
│ 3. Check Redis cache │
│ key: "user:dbId:{clerkUserId}" │
│ if hit → Return cached dbUserId │
│ │
│ 4. Query database (fallback) │
│ SELECT * FROM users WHERE clerk_id = ? │
│ │
│ 5. Update cache & metadata │
│ - Cache to Redis (TTL: 1 hour) │
│ - Update Clerk publicMetadata │
│ │
│ 6. Rate limit check │
│ if exceeded → 429 TOO_MANY_REQUESTS │
│ │
└─────────────────────────────────────────────────────────────────┘
Core Tables:
| Table | Purpose | Key Relationships |
|---|---|---|
users |
User profiles | 1:N → videos, playlists, comments |
videos |
Video metadata | N:1 → users, categories |
categories |
Video categories | 1:N → videos |
subscriptions |
User subscriptions | N:N users (viewer ↔ creator) |
videos_views |
View tracking | N:N users ↔ videos |
video_reactions |
Likes/dislikes | N:N users ↔ videos |
comments |
Comments & replies | N:1 users, videos; self-referencing |
comment_reactions |
Comment reactions | N:N users ↔ comments |
playlists |
User playlists | N:1 users |
playlist_videos |
Playlist contents | N:N playlists ↔ videos |
watch_later |
Watch later list | N:N users ↔ videos |
Schema Highlights:
- Composite Primary Keys for junction tables (subscriptions, reactions)
- Self-referencing in comments table for nested replies
- Visibility Enums for videos and playlists (public/private)
- Cascading Deletes for data integrity
- Timestamps on all tables for soft deletes and auditing
// ✅ Preferred: Server Component for data fetching
// app/(home)/videos/[videoId]/page.tsx
import { HydrateClient, trpc } from "@/trpc/server";
export default async function VideoPage({ params }: { params: { videoId: string } }) {
// Prefetch data on server
void trpc.videos.getOne.prefetch({ id: params.videoId });
return (
<HydrateClient>
<VideoSection videoId={params.videoId} />
</HydrateClient>
);
}// ✅ Client Component only when needed
// moubles/videos/ui/components/video-player.tsx
"use client";
import { useState } from "react";
export function VideoPlayer({ playbackId }: { playbackId: string }) {
const [isPlaying, setIsPlaying] = useState(false);
// Interactive logic here
}// ✅ Database schema with Drizzle
export const videos = pgTable("videos", {
id: uuid("id").primaryKey().defaultRandom(),
title: text("title").notNull(),
visibility: videoVisiblity("visibility").default("private").notNull(),
});
// ✅ API input validation with Zod
export const videosRouter = createTRPCRouter({
getMany: baseProcedure
.input(z.object({
categoryId: z.string().uuid().optional(),
limit: z.number().min(1).max(100).default(10),
}))
.query(async ({ input }) => {
// input is fully typed
}),
});moubles/
└── videos/ # One feature = one module
├── server/
│ └── procedures.ts # tRPC procedures (backend)
├── ui/
│ └── components/ # React components (frontend)
├── views/ # Page-level views
└── type.ts # Shared types
// ✅ Error Boundary for component errors
import { ErrorBoundary } from "react-error-boundary";
<ErrorBoundary fallbackRender={({ error, resetErrorBoundary }) => (
<ErrorFallback error={error} onRetry={resetErrorBoundary} />
)}>
<VideoSection />
</ErrorBoundary>
// ✅ tRPC error handling
try {
const { success } = await ratelimit.limit(userId);
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
} catch (error) {
// Handle gracefully
}// ✅ Optimistic UI updates with TanStack Query
const utils = trpc.useUtils();
const mutation = trpc.subscriptions.create.useMutation({
onMutate: async (input) => {
// Cancel outgoing refetches
await utils.subscriptions.getMany.cancel();
// Snapshot previous value
const previous = utils.subscriptions.getMany.getData();
// Optimistically update
utils.subscriptions.getMany.setData(undefined, (old) => [...old, input]);
return { previous };
},
onError: (err, input, context) => {
// Rollback on error
utils.subscriptions.getMany.setData(undefined, context.previous);
},
});// Component file structure
import { componentVariants } from "./variants"; // CVA variants
import { cn } from "@/lib/utils"; // Class merge utility
interface ComponentProps {
className?: string;
// ... other props
}
export function Component({ className, ...props }: ComponentProps) {
return (
<div className={cn(componentVariants(), className)}>
{/* Component content */}
</div>
);
}// 1. React/Next imports
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
// 2. Third-party libraries
import { useQuery } from "@tanstack/react-query";
// 3. Internal components
import { Button } from "@/components/ui/button";
// 4. Hooks and utilities
import { useAuth } from "@clerk/nextjs";
// 5. Types
import type { Video } from "@/db/schema";| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | VideoPlayer, SubscriptionButton |
| Hooks | camelCase with use prefix |
useIsMobile, useSubscription |
| Utilities | camelCase | formatDuration, cn |
| Constants | SCREAMING_SNAKE_CASE | DEFAULT_PAGE_SIZE |
| Database tables | snake_case (plural) | videos, video_reactions |
| API routes | camelCase | getMany, createSubscription |
| Environment variables | SCREAMING_SNAKE_CASE | DATABASE_URL, MUX_TOKEN_ID |
| File names | kebab-case | video-player.tsx, use-mobile.tsx |
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 15.1.6 | React framework with App Router |
| React | 19.0 | UI library |
| TypeScript | 5.x | Type safety |
| Tailwind CSS | 3.4.1 | Utility-first CSS |
| shadcn/ui | latest | Component library |
| TanStack Query | 5.x | Server state management |
| Lucide React | latest | Icon library |
| Technology | Version | Purpose |
|---|---|---|
| tRPC | 11.x | Type-safe API layer |
| Drizzle ORM | 0.45.1 | Type-safe database ORM |
| Zod | 4.x | Schema validation |
| Service | Purpose |
|---|---|
| Clerk | Authentication & user management |
| Mux | Video processing & streaming |
| UploadThing | File uploads |
| Upstash Redis | Caching & rate limiting |
| QStash | Async task queues |
| Neon | Serverless PostgreSQL |
| Tool | Purpose |
|---|---|
| Bun | JavaScript runtime & package manager |
| ESLint | Code linting |
| Drizzle Kit | Database migrations |
This project is licensed under the MIT License.
MIT License
Copyright (c) 2024 NewTube Clone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Made with ❤️ by the NewTube Team


