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
| Tool | Version | Installation |
|---|---|---|
| Node.js | 18+ | nodejs.org |
| pnpm | 8+ | npm install -g pnpm |
Clone and Install
# Clone the repository
git clone https://github.com/pktikkani/forge.git
cd forge
# Install dependencies
pnpm installEnvironment Setup
Create a .env.local file:
# Copy example env file
cp .env.example .env.local
# Edit with your development credentialsDevelopment 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 studioRunning in Development
# Start development server
pnpm devOpen 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 configArchitecture 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
- 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
}- Push schema changes:
pnpm prisma db push- Create validation schema:
// lib/validations/new-feature.ts
import { z } from "zod";
export const createNewFeatureSchema = z.object({
title: z.string().min(1).max(255),
});- Create API routes:
// app/api/new-feature/route.ts
export async function GET() { ... }
export async function POST() { ... }- Create React hook:
// hooks/use-new-feature.ts
export function useNewFeatures() { ... }
export function useCreateNewFeature() { ... }- 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 formComponents 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:coverageE2E Tests
# Run Playwright tests
pnpm test:e2eCode Quality
Linting
# Run ESLint
pnpm lint
# Fix auto-fixable issues
pnpm lint:fixType Checking
# TypeScript check
pnpm typecheckFormatting
# Format with Prettier
pnpm formatDebugging
Browser DevTools
- Open DevTools:
F12orCmd+Option+I - React DevTools extension for component inspection
- Network tab for API calls
- 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-devGit Workflow
Branch Naming
feature/description- New featuresfix/description- Bug fixesrefactor/description- Code refactoringdocs/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 flowPull Request Process
- Create feature branch from
main - Make changes and test locally
- Push branch and create PR
- Wait for CI checks to pass
- Request review
- 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 buildTroubleshooting
Common Issues
“Cannot find module” errors:
pnpm installPrisma client out of sync:
pnpm prisma generateDatabase connection issues:
# Verify connection
turso db shell forge-dev ".tables"Hot reload not working:
- Check for syntax errors
- Clear
.nextcache:rm -rf .next - Restart dev server
Resources
Documentation
Source Code
Last updated on