A comprehensive guide to understanding how DoubtDesk is structured and how its components work together.
- System Overview
- Architecture Diagram
- Core Components
- Folder Structure Guide
- Data Flow: How a Doubt Travels Through the System
- Key Features
- Database Schema
- Authentication & Authorization
- API Design
- Asynchronous Processing
- Safety & Moderation
- Rate Limiting
- Adding New Features
DoubtDesk is a full-stack Next.js application that bridges students and teachers through AI-powered doubt resolution. The system is designed with three main layers:
- Frontend Layer: React components and pages for students, teachers, and admins
- Backend API Layer: Next.js API routes handling business logic and external integrations
- Data Layer: PostgreSQL database with Drizzle ORM for type-safe queries
The architecture emphasizes:
- Scalability: Classroom-scoped data with multi-tenant support
- Type Safety: TypeScript throughout, Drizzle ORM with strict typing
- Performance: Rate limiting, efficient queries with indexed columns
- Safety: Content moderation, user blocking, audit logging
- Asynchronous Processing: Inngest for background jobs (email notifications, AI processing)
┌──────────────────────────────────────────────────────────────────┐
│ Frontend (Next.js + React) │
│ Pages: Dashboard, Rooms, Profile, Ask AI, Analytics, etc. │
└──────────────────────┬───────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ API Routes (Backend Business Logic) │
│ ├─ /api/doubts (doubt CRUD + retrieval) │
│ ├─ /api/classrooms (classroom management) │
│ ├─ /api/replies (comment/solution handling) │
│ ├─ /api/ask-ai (AI Tutor integration) │
│ ├─ /api/analytics (classroom metrics) │
│ ├─ /api/teacher (teacher tools) │
│ └─ /api/user (profile & preferences) │
│ │
│ Middleware: │
│ ├─ Clerk Auth (authentication) │
│ ├─ Rate Limiting (API protection) │
│ └─ Error Handling (standardized responses) │
└──────────────────────┬───────────────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│Database │ │ Inngest │ │ External │
│(Postgres) │ Queue │ │ Services │
└─────────┘ └──────────┘ │(Groq AI, │
▲ │ │ Google │
│ ▼ │ TTS) │
Drizzle ┌──────────┐ └──────────┘
ORM │Background│
│ Jobs │
└──────────┘
The frontend is built as a server-rendered Next.js application with specialized page directories:
Main Page Directories (app/ folder):
page.tsx- Landing/home page(auth)/- Authentication pages (sign-in, sign-up)(routes)/- Main app routes wrapped in authenticated layoutdashboard/- Student/teacher dashboardrooms/- Virtual classrooms viewask-ai/- AI tutor interfaceprofile/- User profile managementbookmarks/- Saved doubtspublic-rooms/- Public doubt board
Reusable Components (components/ folder):
Sidebar.tsx- NavigationDashboardLayout.tsx- Main layout wrapperDoubtCard.tsx- Displays doubt in feedInfiniteDoubtFeed.tsx- Infinite scroll feedCommandMenu.tsx- Keyboard shortcutsMarkdownRenderer.tsx- Renders formatted doubt contentThemeToggle.tsx- Dark/light mode switcherExportButton.tsx- Export options (PDF/Word)- UI components from
ui/- Buttons, dialogs, forms, etc.
API endpoints follow a RESTful pattern with Clerk authentication:
app/api/
├── doubts/
│ ├── route.ts GET/POST (fetch all, create new)
│ ├── action/
│ │ └── route.ts POST (like, solve, pin, etc.)
│ └── [id]/
│ └── route.ts GET/DELETE (specific doubt)
├── replies/
│ ├── route.ts GET/POST (fetch/create replies)
│ └── [id]/
│ └── route.ts DELETE (remove reply)
├── classrooms/
│ ├── route.ts GET/POST (list/create classrooms)
│ └── [id]/
│ └── route.ts GET/PUT/DELETE (manage classroom)
├── ask-ai/
│ ├── solve/
│ │ └── route.ts POST (AI doubt solver)
│ ├── chat/
│ │ └── route.ts POST (conversational tutor)
│ └── shared-chats/
│ └── route.ts GET (fetch shared chat)
├── analytics/
│ └── route.ts GET (classroom metrics)
├── teacher/
│ ├── analytics/
│ │ └── route.ts GET (performance data)
│ └── moderation/
│ └── route.ts POST (action on violations)
├── user/
│ ├── profile/
│ │ └── route.ts GET/PUT (profile management)
│ └── preferences/
│ └── route.ts PUT (notification settings)
└── inngest/
└── route.ts POST (webhook for background jobs)
The database uses PostgreSQL with Drizzle ORM for type-safe queries. Configuration:
- Connection: Via
configs/db.tsx(pooled for performance) - Schema: Defined in
configs/schema.ts - Migrations: Stored in
drizzle/folder with versioning
Key Design Patterns:
- Cascade DELETE for cleanup (e.g., deleting user removes their memberships)
- SET NULL for anonymization (e.g., deleted user's doubts become anonymous)
- Unique constraints for preventing duplicates
- Indexed columns for performance (email, classroom ID, type, subject)
- Groq AI SDK: Powers the AI doubt solver and conversational tutor
- Google TTS API: Converts text responses to audio
- Integration points:
/api/ask-ai/solve/- Analyze doubt and generate solution/api/ask-ai/chat/- Maintain conversation context
- Rate limited to prevent abuse
- Stored in
lib/ai/with helper functions
Background jobs handled by Inngest:
- Email notifications to teachers/students
- Processing shared chat requests
- Heavy lifting operations
Location: inngest/ folder
client.ts- Inngest client initializationfunctions.ts- Job definitions
DoubtDesk/
├── app/ # Next.js app directory (Pages + API routes)
│ ├── api/ # Backend API routes
│ ├── (auth)/ # Auth pages (sign-in, sign-up)
│ ├── (routes)/ # Protected routes with layout
│ │ ├── dashboard/ # Main dashboard for students/teachers
│ │ ├── rooms/ # Virtual classroom view
│ │ ├── ask-ai/ # AI tutor interface
│ │ ├── profile/ # User profile
│ │ ├── bookmarks/ # Saved doubts
│ │ ├── classrooms/ # Classroom management
│ │ ├── teacher/ # Teacher-specific tools
│ │ └── analytics/ # Analytics dashboard
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ └── provider.tsx # Global providers (theme, auth, etc.)
│
├── components/ # Reusable React components
│ ├── ui/ # UI primitives (buttons, dialogs, etc.)
│ ├── auth/ # Auth-related components
│ ├── resume/ # Resume builder components
│ ├── DashboardLayout.tsx # Main app layout
│ ├── InfiniteDoubtFeed.tsx # Infinite scroll feed
│ ├── DoubtCard.tsx # Doubt display card
│ ├── MarkdownRenderer.tsx # Markdown to HTML renderer
│ ├── CommandMenu.tsx # Keyboard command palette
│ └── ... (other components)
│
├── configs/ # Configuration files
│ ├── db.tsx # Database connection pooling
│ ├── schema.ts # Drizzle ORM schema definitions
│ └── supabase.tsx # Supabase client (storage)
│
├── lib/ # Utility functions and helpers
│ ├── ai/ # AI-related utilities
│ │ └── categorizer.ts # Subject/topic categorization
│ ├── auth-utils.ts # Authentication helpers
│ ├── moderation.ts # Content moderation logic
│ ├── ratelimit.ts # Rate limiting setup
│ ├── error-handler.ts # Standard error responses
│ ├── email.ts # Email templates
│ ├── exportPDF.ts # PDF export logic
│ └── utils.ts # General helpers
│
├── hooks/ # Custom React hooks
│ ├── use-mobile.tsx # Mobile detection
│ └── use-mobile.ts # (alternate version)
│
├── inngest/ # Asynchronous processing
│ ├── client.ts # Inngest client setup
│ └── functions.ts # Background job definitions
│
├── public/ # Static assets
│ ├── temp-assets/ # Temporary assets
│ └── videos/ # Video files
│
├── drizzle/ # Database migrations
│ ├── 0000_bitter_tyger_tiger.sql # Initial schema
│ ├── 0001_add_doubt_tags.sql # Tags feature
│ └── meta/ # Migration metadata
│
├── types/ # TypeScript type definitions
│
├── __tests__/ # Jest test files
│ ├── api/ # API route tests
│ ├── components/ # Component tests
│ └── lib/ # Utility function tests
│
├── scripts/ # Utility scripts
│
├── middleware.tsx # Clerk auth + rate limit middleware
├── next.config.mjs # Next.js configuration
├── tsconfig.json # TypeScript configuration
├── tailwind.config.ts # Tailwind CSS configuration
├── drizzle.config.ts # Drizzle ORM configuration
├── jest.config.ts # Jest testing configuration
├── postcss.config.js # PostCSS configuration
└── package.json # Dependencies and scripts
docs/ # Documentation (YOU ARE HERE)
├── ARCHITECTURE.md # This file
├── CONTRIBUTING.md # Contribution guidelines
└── ... (other docs)
Entry Point: Student navigates to /ask-ai/ or classroom room
User Input (React Form)
↓
POST /api/doubts
├─ Validate input (Zod schema)
├─ Extract user info (Clerk)
├─ Categorize doubt using AI (subject + subtopic)
├─ Content moderation check (profanity, spam, off-topic)
├─ If moderation fails → Log violation, flag doubt as "flagged"
├─ Insert doubt into database
│ └─ Generate unique ID
│ └─ Set classroom scoping (if applicable)
│ └─ Store image (if uploaded) → Supabase
└─ Return doubt with ID to frontend
↓
Frontend shows confirmation
Student sees doubt in their feed
Database Operation:
INSERT INTO doubts (userName, userEmail, classroomId, subject, subTopic, content, imageUrl, type)
VALUES (...)Component: lib/ai/categorizer.ts
The system categorizes doubts to:
- Subject: Math, Physics, Chemistry, Programming, etc.
- SubTopic: Specific topic detected by AI (e.g., "Quadratic Equations")
This enables:
- Better search and filtering
- Grouping similar doubts
- Teacher analytics by subject
If doubt type is "ai", it triggers AI solving:
Doubt Created
↓
POST /api/ask-ai/solve
├─ Extract doubt content
├─ Call Groq AI with doubt context
├─ AI generates solution with steps
├─ (Optional) Generate audio using Google TTS
├─ Store solution as a "reply" with type="solution"
├─ Update doubt status to "isSolved": true
└─ Broadcast to students via WebSocket (if implemented)
↓
Frontend auto-refreshes and shows solution
Integration with Inngest (optional heavy lifting):
Inngest Job Queue
├─ Email notification to doubt creator
├─ Notify teacher if in classroom
├─ Log analytics event
└─ Update teacher dashboard
If doubt type is "community", students/teachers can reply:
Student writes reply
↓
POST /api/replies
├─ Validate reply content
├─ Moderation check
├─ Store reply in database
│ └─ Associate with doubt ID
│ └─ Mark type as "comment" or "solution"
└─ Return reply to frontend
↓
Frontend appends reply to doubt feed
Other students can see and upvote
Teacher Can Mark As Solution:
Teacher views reply
↓
POST /api/doubts/action (action: "solve")
├─ Verify teacher permissions (classroom membership)
├─ Update doubt.solvedReplyId = reply.id
├─ Set doubt.isSolved = "solved"
└─ Notify all students viewing the doubt
User Interaction
├─ Like doubt
│ └─ POST /api/doubts/action (action: "like")
│ ├─ Insert into likes table
│ └─ Increment doubt.likes
│
└─ Bookmark doubt
└─ POST /api/bookmarks
├─ Insert into bookmarks table
└─ Frontend shows bookmark icon
Teacher Dashboard Access
↓
GET /api/analytics (classroomId)
├─ Query: COUNT(doubts) by subject
├─ Query: AVG(time_to_solve) per doubt
├─ Query: Doubts by difficulty (based on response count)
└─ Return metrics for charting
↓
Dashboard displays:
├─ Subject distribution (pie chart)
├─ Solve time trends (line chart)
├─ Top students & helpers
└─ Unsolved doubt alerts
What: Teachers can create isolated spaces for their classes
Database Tables:
classrooms- Classroom metadatamemberships- Many-to-many user-classroom relationship
Flow:
- Teacher creates classroom → Generates unique invite code
- Students join via code → Inserted into memberships
- Doubts posted in room are scoped to
classroomId - Only members can view/participate
- When classroom deleted → Doubts become public (SET NULL)
Security:
- Membership check on every API call
- Role-based access (student/teacher/admin)
- Separate analytics per classroom
Purpose: Protect community from harassment, spam, and harmful content
Components:
lib/moderation.ts- Profanity detection, spam filteringmoderationLogsTable- Audit trail of violationsusersTable.violationCount- Track repeat offenders
Flow:
Doubt/Reply Posted
↓
Run moderation checks:
├─ Profanity filter
├─ Spam detection
├─ Off-topic detection
└─ Toxicity scoring
↓
If violation detected:
├─ Log violation (moderationLogsTable)
├─ Increment user.violationCount
├─ Mark content as flagged (pending admin review)
├─ If violationCount ≥ 3:
│ └─ Block user for 48 hours (user.isBlocked = true)
└─ Notify admin
↓
Admin actions:
├─ Approve content → Remove flag
├─ Remove content → Delete from database
└─ Warn/suspend user
Escalation:
- Level 1: 1 violation → Warning
- Level 2: 2 violations → 24h block
- Level 3: 3+ violations → 48h+ block, manual review
For Teachers: Understand student learning patterns
Metrics Tracked:
- Doubts by subject
- Average resolution time
- Most active students
- Trending topics
- Unsolved doubt alerts
Aggregation:
- Classroom-level rollup
- Subject-level breakdown
- Time-series trends
Purpose: Students can save doubts for later study
Flow:
- Student clicks bookmark icon on doubt
- POST
/api/bookmarksinserts intobookmarkstable - Frontend fetches
/bookmarksroute to show saved doubts - Cascade delete when doubt removed
users
- Stores user profile and security info
- Role: student, teacher, admin
- Moderation fields: violationCount, isBlocked, blockedUntil
classrooms
- Created by teachers
- Each has unique invite code
- Contains: name, university, year, teacherEmail
memberships
- Links users to classrooms
- Roles per classroom: student, teacher, admin
- Unique constraint: one membership per user per classroom
doubts
- Core table storing student questions
- Status: unsolved, solved
- Type: ai, community, teacher
- Scoped by classroomId (null for public)
- Categorization: subject, subTopic
- Media: imageUrl (stored in Supabase)
replies
- Responses to doubts
- Type: comment, solution
- Can be upvoted
- Cascade deleted with parent doubt
tags
- Metadata labels for doubts
- Classroom-scoped or global
- Normalized name for search
doubt_tags
- Many-to-many relationship
- Links doubts to tags
chat_history
- Persistent AI conversation state
- Allows continuing chat sessions
- Grouped by chatId
moderation_logs
- Audit trail of violations
- Records violation type and content snippet
- Used for admin review and pattern detection
Indexes:
-- Query optimization
CREATE INDEX doubt_classroomId_idx ON doubts(classroomId);
CREATE INDEX type_idx ON doubts(type);
CREATE INDEX subject_idx ON doubts(subject);
CREATE INDEX doubtId_idx ON replies(doubtId);
CREATE INDEX userEmail_idx ON memberships(userEmail);Unique Constraints:
- Only one membership per user per classroom
- Only one bookmark per user per doubt
- Only one like per user per doubt
Setup:
- Configured in
middleware.tsx - Protected routes with
createRouteMatcher - Public routes: home, sign-in/up, public boards
Public Routes:
/ (home)
/sign-in (auth)
/sign-up (auth)
/public-rooms (read-only doubt board)
/api/inngest (webhook)
Protected Routes:
/dashboard (requires auth)
/profile (requires auth)
/rooms (requires auth)
/ask-ai (requires auth)
Student:
- Create doubts
- Reply to doubts
- Like/bookmark
- View classroom doubts (if member)
- View own analytics
Teacher:
- Create classrooms
- Invite students
- Mark solutions as correct
- View classroom analytics
- Moderate content
- Pin/feature doubts
Admin:
- All teacher permissions
- Global moderation
- User management
- System analytics
Standard Success Response:
{
"success": true,
"data": { /* entity data */ },
"message": "Operation completed"
}Error Response:
{
"error": "Error code",
"message": "Human-readable error",
"details": "Additional context"
}GET /api/doubts?subject=Math&classroomId=5&type=community&tag=algebra
subject: Filter by subject (Math, Physics, etc.)classroomId: Scope to classroom (null for public)type: Filter by type (ai, community, teacher)tag: Filter by tag nameuserName: Search by student name (optional)
Most list endpoints support cursor-based pagination:
GET /api/doubts?limit=10&offset=0
Background jobs for heavy/async work:
Setup:
- Inngest client:
inngest/client.ts - Function definitions:
inngest/functions.ts - API webhook:
app/api/inngest/route.ts
Example Job: Notify Teacher of Doubt:
// inngest/functions.ts
export const notifyTeacherOfDoubt = inngest.createFunction(
{ id: "notify-teacher-doubt" },
{ event: "doubt/created" },
async ({ event }) => {
const doubt = event.data;
const classroom = await db.select()
.from(classroomsTable)
.where(eq(classroomsTable.id, doubt.classroomId));
await sendEmail({
to: classroom.teacherEmail,
subject: `New doubt in ${classroom.name}`,
html: renderDoubtNotification(doubt),
});
}
);Triggering:
// Inside API route
await inngest.send({
name: "doubt/created",
data: { doubtId: 123, classroomId: 5 },
});Steps:
- Input Validation: Zod schema ensures valid format
- Profanity Filter: Check against banned word list
- Spam Detection: Flag excessive punctuation, repeated messages
- Length Check: Doubts must be reasonable length
- Topic Detection: Flag completely off-topic content
- Toxicity Scoring: AI-powered sentiment analysis
Trigger:
- 3+ moderation violations in 30 days
- Manual admin action
Effect:
- User cannot create doubts/replies
- User can still view (read-only mode)
- Block expires after 48 hours (auto-reset)
Storage:
UPDATE users
SET isBlocked = true, blockedUntil = NOW() + INTERVAL '48 hours'
WHERE id = $1;Two-tier rate limiting in middleware.tsx:
- General API Limit: 100 requests per minute per IP
- AI Route Limit: 10 requests per minute per IP (stricter)
Implementation:
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
const generalLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(100, "60 s"),
});
const aiLimiter = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.fixedWindow(10, "60 s"),
});Response:
{
"error": "Too many requests",
"retryAfter": "45 seconds"
}File: configs/schema.ts
import { pgTable, varchar, timestamp, foreignKey } from "drizzle-orm/pg-core";
export const myNewTable = pgTable("my_new_table", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
userEmail: varchar({ length: 255 }).notNull(),
data: text(),
createdAt: timestamp().defaultNow(),
}, (table) => ({
userEmailFk: foreignKey({
columns: [table.userEmail],
foreignColumns: [usersTable.email],
}).onDelete("cascade"),
}));Generate Migration:
npm run db:migrate
# Follow prompts to name your migrationFile: app/api/my-feature/route.ts
import { db } from "@/configs/db";
import { myNewTable } from "@/configs/schema";
import { currentUser } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
export async function GET(req: Request) {
try {
const user = await currentUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const email = user.primaryEmailAddress?.emailAddress;
const data = await db.select()
.from(myNewTable)
.where(eq(myNewTable.userEmail, email));
return NextResponse.json({ success: true, data });
} catch (error) {
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function POST(req: Request) {
try {
const user = await currentUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const email = user.primaryEmailAddress?.emailAddress;
const [newRecord] = await db.insert(myNewTable)
.values({
userEmail: email,
data: body.data,
})
.returning();
return NextResponse.json({ success: true, data: newRecord });
} catch (error) {
return NextResponse.json(
{ error: "Failed to create record" },
{ status: 400 }
);
}
}File: components/MyFeature.tsx
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
export function MyFeature() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
const res = await fetch("/api/my-feature");
const json = await res.json();
setData(json.data);
setLoading(false);
};
fetchData();
}, []);
return (
<div>
{loading ? <p>Loading...</p> : (
<ul>
{data.map((item) => (
<li key={item.id}>{item.data}</li>
))}
</ul>
)}
<Button onClick={() => {/* create logic */}}>Add New</Button>
</div>
);
}Create Page: app/(routes)/my-feature/page.tsx
import { MyFeature } from "@/components/MyFeature";
export default function MyFeaturePage() {
return (
<div className="container mx-auto p-4">
<h1 className="text-3xl font-bold mb-4">My Feature</h1>
<MyFeature />
</div>
);
}File: __tests__/api/my-feature.test.ts
import { GET, POST } from "@/app/api/my-feature/route";
describe("My Feature API", () => {
it("should fetch data", async () => {
const req = new Request("http://localhost/api/my-feature");
const res = await GET(req);
expect(res.status).toBe(200);
});
it("should create new record", async () => {
const req = new Request("http://localhost/api/my-feature", {
method: "POST",
body: JSON.stringify({ data: "test" }),
});
const res = await POST(req);
expect(res.status).toBe(200);
});
});- Add feature description to README.md
- Document new API endpoints in API_DOCS (if exists)
- Add database diagram updates
# Test locally
npm run dev
# Build
npm run build
# Deploy to Vercel
git push origin feature-branch
# Vercel auto-deploysimport { buildErrorResponse } from "@/lib/error-handler";
try {
// code
} catch (error) {
return NextResponse.json(
buildErrorResponse("operation_failed", "Could not perform operation"),
{ status: 500 }
);
}import { db } from "@/configs/db";
import { doubtsTable } from "@/configs/schema";
import { eq, and, or, desc } from "drizzle-orm";
// Single query
const [doubt] = await db.select()
.from(doubtsTable)
.where(eq(doubtsTable.id, doubtId));
// Conditional query
const doubts = await db.select()
.from(doubtsTable)
.where(
and(
eq(doubtsTable.classroomId, classroomId),
eq(doubtsTable.isSolved, "unsolved")
)
)
.orderBy(desc(doubtsTable.createdAt))
.limit(10);
// Join
const doubtsWithReplies = await db.select()
.from(doubtsTable)
.leftJoin(repliesTable, eq(doubtsTable.id, repliesTable.doubtId))
.where(eq(doubtsTable.id, doubtId));// Define function
export const myBackgroundJob = inngest.createFunction(
{ id: "my-job" },
{ event: "my/event" },
async ({ event, step }) => {
// Step 1: Do something
await step.run("step-1", async () => {
// Your logic
});
// Step 2: Wait and do more
await step.sleep("wait", "1 hour");
await step.run("step-2", async () => {
// More logic
});
}
);
// Trigger from API
await inngest.send({
name: "my/event",
data: { userId: 123 },
});Cause: User not a member of the classroom
Solution: Check memberships table - user's email must be in table with classroom ID
Cause: Moderation rules flagging legitimate content
Solution: Check lib/moderation.ts - may need to tune thresholds
Cause: Too many requests from same IP
Solution: Check middleware.tsx - AI routes limited to 10/min, general 100/min
Cause: Schema drift detected
Solution: Run npm run db:reset (dev only) or manually fix schema
- Database Schema: configs/schema.ts
- API Routes: app/api/
- Component Examples: components/
- Contributing Guide: CONTRIBUTING.md
- Main README: README.md
Open an issue on GitHub or reach out to the maintainers. This documentation is a living document and will be updated as the architecture evolves.