← Writing
webnextjstypescriptmongodbprismanextauthtailwind

Building Bluedit: A Full-Stack Reddit Clone with Next.js and MongoDB

A Reddit-like forum with subreddits, voting, threaded comments, and user profiles — built with Next.js App Router, Prisma ORM, MongoDB, and NextAuth for OAuth authentication.

·2 views

Bluedit is an open-source Reddit clone. It has subreddits, posts (called threads), nested comments, voting, user profiles with follow/follower graphs, and OAuth login. The stack: Next.js 14 App Router, Prisma ORM backed by MongoDB, NextAuth, and Tailwind CSS.

Source: github.com/YuqiaoSu/bluedit

Data Model

The schema centers on four main models:

User — auth identity (username, hashed password, role, avatar). Role distinguishes regular users from moderators/admins.

Profile — extends User with social data: bio, display name, banner image, and bidirectional follow lists (following[], follower[]). Stored as relations on the Profile model rather than on User, keeping auth data separate from social graph data.

Thread — a post in a subreddit. Fields: title, content (markdown), subreddit reference, author, publish state, and upvote count. The published flag lets authors save drafts before posting.

Comment — nested comment tree. Each comment has a parent reference (null for top-level), enabling arbitrary depth threading.

model Thread {
  id          String    @id @default(auto()) @map("_id") @db.ObjectId
  title       String
  content     String
  published   Boolean   @default(false)
  upvote      Int       @default(0)
  author      User      @relation(fields: [authorId], references: [id])
  authorId    String    @db.ObjectId
  subreddit   Subreddit @relation(fields: [subredditId], references: [id])
  comments    Comment[]
  createdAt   DateTime  @default(now())
}

Routing (App Router)

Next.js App Router gives each route a page.tsx (server component by default):

app/
├── page.tsx                  # home feed
├── r/[subreddit]/page.tsx    # subreddit feed
├── r/[subreddit]/[threadId]/ # thread + comments
├── u/[username]/page.tsx     # user profile
├── search/page.tsx           # search results
├── login/page.tsx
├── register/page.tsx
└── admin/page.tsx            # mod tools

Server components fetch data directly — no useEffect, no client-side loading states for the initial render. Mutations (votes, comments, follows) use Server Actions, keeping API boilerplate minimal.

Authentication

NextAuth handles OAuth and credential-based login. Session data flows through the app via the useSession hook on the client and getServerSession on the server. The role field on User is embedded in the JWT so authorization checks don't require a DB round-trip.

// Server component — no extra API call needed
const session = await getServerSession(authOptions);
if (session?.user?.role !== "admin") redirect("/");

Prisma + MongoDB

Prisma's MongoDB adapter works differently from its relational drivers: MongoDB uses _id (ObjectId) instead of auto-increment integers, and relations are stored as embedded documents or referenced arrays rather than foreign key joins.

The Prisma schema handles this via @db.ObjectId and bidirectional relation arrays on the Profile model for follow graphs — storing follower/following IDs as arrays on both sides and keeping them in sync in a transaction.

What I Learned

App Router's server-component-first model changes how you think about data fetching. The instinct from Pages Router is to reach for getServerSideProps or useEffect — in App Router, the right default is to make the component async and await the DB query inline. Client components become the exception for interactivity, not the rule.

Prisma's type generation from the schema is genuinely useful: the TypeScript compiler catches mismatches between query return types and the components that consume them, before they become runtime errors.

Comments

Loading…

0/300 words