Skip to Content
WebForge Web - Development Guide

Forge Web - Development Guide

This guide covers setting up the development environment, understanding the codebase structure, and contributing to Forge Web.


Development Environment Setup

Prerequisites

ToolVersionInstallation
Node.js18+nodejs.org 
pnpm8+npm install -g pnpm

Clone and Install

# Clone the repository git clone https://github.com/pktikkani/forge.git cd forge # Install dependencies pnpm install

Environment Setup

Create a .env.local file:

# Copy example env file cp .env.example .env.local # Edit with your development credentials

Development Environment Variables:

# Authentication AUTH_SECRET="dev-secret-change-in-production" AUTH_MICROSOFT_ENTRA_ID_ID="your-azure-dev-client-id" AUTH_MICROSOFT_ENTRA_ID_SECRET="your-azure-dev-secret" AUTH_MICROSOFT_ENTRA_ID_ISSUER="https://login.microsoftonline.com/your-tenant/v2.0" # Database (use a dev database!) TURSO_DATABASE_URL="libsql://forge-dev-yourname.turso.io" TURSO_AUTH_TOKEN="your-dev-token" # Application NEXT_PUBLIC_APP_URL="http://localhost:3000" # LiveKit (optional for development) LIVEKIT_URL="wss://your-dev.livekit.cloud" LIVEKIT_API_KEY="your-dev-key" LIVEKIT_API_SECRET="your-dev-secret"

Database Setup

# Generate Prisma client pnpm prisma generate # Push schema to database pnpm prisma db push # (Optional) Open Prisma Studio pnpm prisma studio

Running in Development

# Start development server pnpm dev

Open http://localhost:3000  in your browser.


Project Structure

forge/ ├── src/ │ ├── app/ # Next.js App Router │ │ ├── (auth)/ # Auth pages (sign-in, sign-out) │ │ │ └── auth/ │ │ │ └── signin/ │ │ ├── api/ # API routes │ │ │ ├── activity/ # Activity feed endpoints │ │ │ ├── auth/ # NextAuth handlers │ │ │ ├── comments/ # Comments CRUD │ │ │ ├── goals/ # Goals CRUD │ │ │ ├── ideas/ # Ideas CRUD │ │ │ ├── livekit/ # Voice/video calling │ │ │ ├── mindmaps/ # Mind map data │ │ │ ├── search/ # Global search │ │ │ ├── tasks/ # Tasks CRUD │ │ │ └── users/ # User management │ │ ├── dashboard/ # Main application UI │ │ │ ├── ideas/ │ │ │ ├── goals/ │ │ │ ├── tasks/ │ │ │ └── page.tsx │ │ ├── layout.tsx # Root layout │ │ └── page.tsx # Landing page │ │ │ ├── components/ # React components │ │ ├── ui/ # shadcn/ui primitives │ │ ├── calling/ # LiveKit calling UI │ │ ├── dashboard/ # Dashboard components │ │ ├── goals/ # Goals module │ │ ├── ideas/ # Ideas module │ │ ├── mindmap/ # React Flow mind maps │ │ ├── providers/ # Context providers │ │ ├── search/ # Search palette │ │ ├── shared/ # Shared components │ │ └── tasks/ # Kanban board │ │ │ ├── hooks/ # Custom React hooks │ │ ├── use-ideas.ts │ │ ├── use-goals.ts │ │ ├── use-tasks.ts │ │ ├── use-call.ts │ │ └── ... │ │ │ ├── stores/ # Zustand state stores │ │ ├── call-store.ts │ │ └── ui-store.ts │ │ │ ├── lib/ # Utilities and configuration │ │ ├── auth.ts # NextAuth configuration │ │ ├── auth.config.ts # Auth providers config │ │ ├── db.ts # Prisma client │ │ ├── livekit.ts # LiveKit utilities │ │ ├── logger.ts # Pino logger │ │ ├── utils.ts # Helper functions │ │ └── validations/ # Zod schemas │ │ │ ├── types/ # TypeScript definitions │ │ └── index.ts │ │ │ └── middleware.ts # NextAuth middleware ├── prisma/ │ └── schema.prisma # Database schema ├── public/ # Static assets ├── package.json ├── next.config.ts ├── tailwind.config.ts ├── tsconfig.json └── components.json # shadcn/ui config

Architecture Deep Dive

Frontend State Management

React Query - Server state (Ideas, Goals, Tasks)

// hooks/use-ideas.ts export function useIdeas() { return useQuery({ queryKey: ["ideas"], queryFn: async () => { const res = await fetch("/api/ideas"); return res.json(); }, }); } export function useCreateIdea() { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (data: CreateIdeaInput) => { const res = await fetch("/api/ideas", { method: "POST", body: JSON.stringify(data), }); return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["ideas"] }); }, }); }

Zustand - Client state (UI, Calling)

// stores/call-store.ts interface CallStore { activeCall: Call | null; incomingCall: Call | null; setActiveCall: (call: Call | null) => void; setIncomingCall: (call: Call | null) => void; } export const useCallStore = create<CallStore>((set) => ({ activeCall: null, incomingCall: null, setActiveCall: (call) => set({ activeCall: call }), setIncomingCall: (call) => set({ incomingCall: call }), }));

API Routes Pattern

// app/api/ideas/route.ts import { auth } from "@/lib/auth"; import { prisma } from "@/lib/db"; import { createIdeaSchema } from "@/lib/validations/idea"; export async function GET() { const session = await auth(); if (!session?.user) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const ideas = await prisma.idea.findMany({ where: { author: { tenantId: session.user.tenantId, }, }, include: { author: true, _count: { select: { comments: true } }, }, orderBy: { createdAt: "desc" }, }); return Response.json(ideas); } export async function POST(req: Request) { const session = await auth(); if (!session?.user) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } const body = await req.json(); const validated = createIdeaSchema.parse(body); const idea = await prisma.idea.create({ data: { ...validated, authorId: session.user.id, }, }); // Log activity await prisma.auditLog.create({ data: { action: "created", entityType: "idea", entityId: idea.id, entityTitle: idea.title, authorId: session.user.id, }, }); return Response.json(idea); }

Authentication Flow

// lib/auth.ts import NextAuth from "next-auth"; import { PrismaAdapter } from "@auth/prisma-adapter"; import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; import { prisma } from "./db"; export const { handlers, signIn, signOut, auth } = NextAuth({ adapter: PrismaAdapter(prisma), providers: [ MicrosoftEntraID({ clientId: process.env.AUTH_MICROSOFT_ENTRA_ID_ID!, clientSecret: process.env.AUTH_MICROSOFT_ENTRA_ID_SECRET!, issuer: process.env.AUTH_MICROSOFT_ENTRA_ID_ISSUER!, }), ], callbacks: { async session({ session, user }) { // Add tenantId to session const dbUser = await prisma.user.findUnique({ where: { id: user.id }, }); session.user.tenantId = dbUser?.tenantId; return session; }, }, });

Common Development Tasks

Adding a New Feature

  1. Define the Prisma model (if needed):
// prisma/schema.prisma model NewFeature { id String @id @default(cuid()) title String authorId String author User @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
  1. Push schema changes:
pnpm prisma db push
  1. Create validation schema:
// lib/validations/new-feature.ts import { z } from "zod"; export const createNewFeatureSchema = z.object({ title: z.string().min(1).max(255), });
  1. Create API routes:
// app/api/new-feature/route.ts export async function GET() { ... } export async function POST() { ... }
  1. Create React hook:
// hooks/use-new-feature.ts export function useNewFeatures() { ... } export function useCreateNewFeature() { ... }
  1. Create UI components:
// components/new-feature/new-feature-list.tsx export function NewFeatureList() { ... }

Adding a shadcn/ui Component

# Add component pnpm dlx shadcn@latest add button pnpm dlx shadcn@latest add dialog pnpm dlx shadcn@latest add form

Components are added to src/components/ui/.

Working with the Mind Map

The mind map uses React Flow:

// components/mindmap/idea-map.tsx import { ReactFlow, Node, Edge } from "@xyflow/react"; export function IdeaMap({ ideas }: { ideas: Idea[] }) { const nodes: Node[] = ideas.map((idea) => ({ id: idea.id, type: "ideaNode", position: { x: 0, y: 0 }, data: { idea }, })); return ( <ReactFlow nodes={nodes} edges={edges} nodeTypes={nodeTypes} fitView /> ); }

Working with Drag and Drop

The Kanban board uses @dnd-kit:

// components/tasks/task-board.tsx import { DndContext, DragOverlay, closestCorners, } from "@dnd-kit/core"; export function TaskBoard() { const [activeId, setActiveId] = useState<string | null>(null); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { // Update task status updateTask.mutate({ id: active.id, status: over.id, }); } }; return ( <DndContext collisionDetection={closestCorners} onDragEnd={handleDragEnd} > {columns.map((column) => ( <TaskColumn key={column.id} column={column} /> ))} </DndContext> ); }

Testing

Unit Tests

# Run tests pnpm test # Run with coverage pnpm test:coverage

E2E Tests

# Run Playwright tests pnpm test:e2e

Code Quality

Linting

# Run ESLint pnpm lint # Fix auto-fixable issues pnpm lint:fix

Type Checking

# TypeScript check pnpm typecheck

Formatting

# Format with Prettier pnpm format

Debugging

Browser DevTools

  1. Open DevTools: F12 or Cmd+Option+I
  2. React DevTools extension for component inspection
  3. Network tab for API calls
  4. Console for errors

Server-Side Logging

import { logger } from "@/lib/logger"; logger.info({ userId }, "User logged in"); logger.error({ error }, "Failed to create idea");

View logs in terminal or configure log aggregation.

Database Debugging

# Open Prisma Studio pnpm prisma studio # Query database directly turso db shell forge-dev

Git Workflow

Branch Naming

  • feature/description - New features
  • fix/description - Bug fixes
  • refactor/description - Code refactoring
  • docs/description - Documentation

Commit Messages

Follow conventional commits:

feat: add user presence indicator fix: resolve task drag-drop issue docs: update development guide refactor: simplify auth flow

Pull Request Process

  1. Create feature branch from main
  2. Make changes and test locally
  3. Push branch and create PR
  4. Wait for CI checks to pass
  5. Request review
  6. Merge after approval

Performance Tips

React Query Optimization

// Avoid over-fetching const { data } = useQuery({ queryKey: ["ideas"], queryFn: fetchIdeas, staleTime: 5 * 60 * 1000, // Don't refetch for 5 minutes }); // Prefetch on hover const prefetchIdea = (id: string) => { queryClient.prefetchQuery({ queryKey: ["idea", id], queryFn: () => fetchIdea(id), }); };

Component Optimization

React 19’s compiler handles most optimizations automatically. For manual cases:

// Memoize expensive computations const sortedIdeas = useMemo(() => ideas.sort((a, b) => a.createdAt - b.createdAt), [ideas] );

Bundle Analysis

# Analyze bundle size ANALYZE=true pnpm build

Troubleshooting

Common Issues

“Cannot find module” errors:

pnpm install

Prisma client out of sync:

pnpm prisma generate

Database connection issues:

# Verify connection turso db shell forge-dev ".tables"

Hot reload not working:

  • Check for syntax errors
  • Clear .next cache: rm -rf .next
  • Restart dev server

Resources

Documentation

Source Code

Last updated on