Thought Tap: Threads Clone
Full-stack Threads clone featuring nested comment tree hierarchies, user profiles, Clerk auth, and MongoDB document relationships.





Thought Tap is a full-stack social media platform inspired by Threads (Meta), built as a deep dive into Next.js 13's App Router, Server Components model, and complex NoSQL relational document modeling.
System Architecture
The project connects client-side interface actions (threads creation, nested replies, joins) directly to server-side mutations, utilizing Clerk for secure token-based user sessions and UploadThing for efficient, optimized file streaming.
Why Build a Threads Clone?
When Meta launched Threads and it hit 100 million sign-ups in 5 days, I wanted to understand the engineering behind that kind of platform. How do you model nested threads? How do you handle recursive replies? How does MongoDB handle deeply populated schemas without killing performance?
MongoDB Schema Design
The trickiest part was the nested thread model. A thread can have replies, and those replies can have replies (infinite nesting):
const threadSchema = new Schema({
text: { type: String, required: true },
author: { type: Schema.Types.ObjectId, ref: 'User', required: true },
community: { type: Schema.Types.ObjectId, ref: 'Community' },
parentId: { type: Schema.Types.ObjectId, ref: 'Thread' },
children: [{ type: Schema.Types.ObjectId, ref: 'Thread' }],
createdAt: { type: Date, default: Date.now },
});
// Recursive population for nested replies
async function fetchThreadWithReplies(threadId: string) {
return Thread.findById(threadId)
.populate({
path: 'children',
populate: {
path: 'author',
select: 'name image username'
}
})
.populate('author', 'name image username');
}Clerk Webhook Synchronization
To ensure that the user identity matches between Clerk auth tokens and the local MongoDB database, a custom /api/webhook/clerk endpoint verifies signed events from Clerk using svix:
import { Webhook } from 'svix';
import { headers } from 'next/headers';
export async function POST(req: Request) {
const payload = await req.json();
const headerPayload = headers();
const svix_id = headerPayload.get("svix-id");
const svix_signature = headerPayload.get("svix-signature");
const svix_timestamp = headerPayload.get("svix-timestamp");
if (!svix_id || !svix_signature || !svix_timestamp) {
return new Response('Error occured -- no svix headers', {
status: 400
});
}
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET || '');
let evt: any;
try {
evt = wh.verify(JSON.stringify(payload), {
"svix-id": svix_id,
"svix-signature": svix_signature,
"svix-timestamp": svix_timestamp,
});
} catch (err) {
return new Response('Error occured', { status: 400 });
}
// Handle sync logic based on evt.type (user.created, user.updated, user.deleted)
}Next.js 13 Server Components
The thread feed uses Server Components to eliminate client-side data fetching. Threads are fetched directly from MongoDB in the server component and streamed to the browser as pre-rendered HTML:
// app/page.tsx - Server Component
export default async function Home({ searchParams }) {
const { threads, isNext } = await fetchPosts(
searchParams.page ? +searchParams.page : 1,
30
);
return (
<section>
<ThreadsTab threads={threads} />
<Pagination isNext={isNext} pageNumber={searchParams.page || 1} />
</section>
);
}Features Built
- Nested Thread Replies: Recursive rendering UI up to arbitrary tree depths.
- Community Management: Users can create, join, invite, and publish threads specifically to a group feed.
- Unified Media Upload: Image attachments are handled seamlessly using UploadThing.
- Modern Auth Integration: Embedded Clerk sign-up / sign-in with full webhook database syncing.
- Zod Form Validation: Secure schema validation on the client and server.
- Real-Time Webhooks: Listening to post events to send browser notifications.