Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Full-Stack SaaS Application

A complete task management system for remote teams with authentication, real-time updates, and role-based access control.


Overview

TaskFlow is a collaborative task management application similar to Trello or Asana, built from scratch using agentful in 6-8 hours.

Business Value

  • Target Users: Remote teams, startups, agencies
  • Key Problem: Managing tasks across distributed teams
  • Solution: Centralized task management with real-time collaboration

Key Features

  • ✅ JWT-based authentication
  • ✅ Team workspace management
  • ✅ Task CRUD operations
  • ✅ Real-time task updates
  • ✅ Role-based permissions (Admin, Member, Viewer)
  • ✅ Task assignments and due dates
  • ✅ Activity feed

Complete PRODUCT.md

Here's the complete PRODUCT.md that powered this project, organized in a hierarchical structure perfect for production applications:

# TaskFlow - Team Task Management
 
## Overview
A collaborative task management application for remote teams. Users can create workspaces, invite team members, create and assign tasks, and track progress with real-time updates.
 
## Tech Stack
- **Frontend**: Next.js 14 (App Router), TypeScript, Tailwind CSS
- **Backend**: Next.js API Routes, Prisma ORM
- **Database**: PostgreSQL (local Docker or Supabase)
- **Auth**: JWT with httpOnly cookies
- **State**: Zustand for client state, React Query for server state
- **Testing**: Vitest, Playwright, Testing Library
- **Validation**: Zod
- **Styling**: Tailwind CSS + shadcn/ui components
 
## Features
 
<FEATURE_ORGANIZATION NOTE>
This example demonstrates HIERARCHICAL structure for production apps.
Instead of one massive PRODUCT.md, features are organized by domain:
- .claude/product/domains/authentication/
- .claude/product/domains/workspaces/
- .claude/product/domains/tasks/
This scales better for large applications with multiple teams.
</FEATURE_ORGANIZATION NOTE>
 
### Domain: Authentication
 
#### User Registration - CRITICAL
**Priority**: CRITICAL - Must be completed first
**User Stories**:
- As a new user, I can register with email and password
**Acceptance Criteria**:
- Passwords must be hashed (bcrypt, 10 rounds)
- Email validation required
- User automatically logged in after registration
- JWT token stored in httpOnly cookie
**API Endpoints**:
- POST /api/auth/register
**Components**:
- app/(auth)/register/page.tsx
- components/auth/RegisterForm.tsx
 
#### User Login - CRITICAL
**Priority**: CRITICAL
**User Stories**:
- As a registered user, I can login to access my workspace
**Acceptance Criteria**:
- JWT tokens stored in httpOnly cookies
- Session expires after 7 days
- Protected routes redirect to login
- Login form shows clear error messages
**API Endpoints**:
- POST /api/auth/login
- GET /api/auth/me
**Components**:
- app/(auth)/login/page.tsx
- components/auth/LoginForm.tsx
 
#### User Logout - CRITICAL
**Priority**: CRITICAL
**User Stories**:
- As a logged-in user, I can logout securely
**Acceptance Criteria**:
- Clears httpOnly cookie
- Redirects to login page
- Clears client-side state
**API Endpoints**:
- POST /api/auth/logout
**Components**:
- components/auth/LogoutButton.tsx
 
#### User Profile - LOW
**Priority**: LOW - Nice to have
**User Stories**:
- As a user, I can view my profile
- As a user, I can update my name and avatar
**Acceptance Criteria**:
- Profile shows name, email, avatar
- Avatar upload to cloud storage (or use Gravatar)
- Email changes require verification
**API Endpoints**:
- GET /api/users/me
- PUT /api/users/me
**Components**:
- app/profile/page.tsx
- components/users/ProfileForm.tsx
 
### Domain: Workspaces
 
#### Create Workspace - HIGH
**Priority**: HIGH - Core feature for organizing work
**User Stories**:
- As a user, I can create a new workspace
**Acceptance Criteria**:
- User can create unlimited workspaces
- Workspace requires name and description
- Creator becomes workspace owner
**API Endpoints**:
- POST /api/workspaces
**Components**:
- components/workspaces/CreateWorkspaceDialog.tsx
 
#### View Workspaces - HIGH
**Priority**: HIGH
**User Stories**:
- As a workspace member, I can view all workspaces I'm part of
**Acceptance Criteria**:
- Show list of all user's workspaces
- Display workspace name and member count
- Click to enter workspace
**API Endpoints**:
- GET /api/workspaces
**Components**:
- components/workspaces/WorkspaceList.tsx
- components/workspaces/WorkspaceCard.tsx
 
#### Manage Workspace Members - MEDIUM
**Priority**: MEDIUM
**User Stories**:
- As a workspace owner, I can invite team members
- As a workspace owner, I can remove members
**Acceptance Criteria**:
- Members invited via email
- Members have roles: Owner, Admin, Member, Viewer
- Email invitation sent with workspace link
**API Endpoints**:
- POST /api/workspaces/:id/members
- DELETE /api/workspaces/:id/members/:userId
**Components**:
- components/workspaces/MemberList.tsx
- components/workspaces/InviteMemberDialog.tsx
 
#### Real-time Presence - MEDIUM
**Priority**: MEDIUM - Enhances collaboration
**User Stories**:
- As a user, I see when team members come online
**Acceptance Criteria**:
- Display online/offline status
- Show active users in workspace
- Update status in real-time
**Implementation**:
- /api/workspaces/:id/events (SSE endpoint)
- Hook: hooks/useWorkspacePresence.ts
- Component: components/workspaces/OnlineUsers.tsx
 
### Domain: Tasks
 
#### Create Task - HIGH
**Priority**: HIGH - Core feature
**User Stories**:
- As a user, I can create tasks in a workspace
**Acceptance Criteria**:
- Tasks require title and workspace
- Optional: description, assignee, due date, priority
- Default status: Todo
**API Endpoints**:
- POST /api/workspaces/:workspaceId/tasks
**Components**:
- components/tasks/CreateTaskDialog.tsx
 
#### View Tasks - HIGH
**Priority**: HIGH
**User Stories**:
- As a user, I can view all tasks in a workspace
**Acceptance Criteria**:
- Tasks organized by status (Todo, In Progress, Done)
- Kanban-style board layout
- Drag-and-drop to move tasks
**API Endpoints**:
- GET /api/workspaces/:workspaceId/tasks
**Components**:
- components/tasks/TaskBoard.tsx
- components/tasks/TaskCard.tsx
 
#### Update Task Status - HIGH
**Priority**: HIGH
**User Stories**:
- As a user, I can move tasks through statuses
**Acceptance Criteria**:
- Drag and drop to change status
- Optimistic UI update
- Activity feed tracks changes
**API Endpoints**:
- PATCH /api/tasks/:id/status
**Components**:
- components/tasks/TaskCard.tsx (with drag handlers)
 
#### Assign Tasks - MEDIUM
**Priority**: MEDIUM
**User Stories**:
- As a user, I can assign tasks to team members
- As a user, I can set due dates and priorities
**Acceptance Criteria**:
- Assign from workspace member list
- Set priority: Low, Medium, High, Urgent
- Due date picker
**API Endpoints**:
- PUT /api/tasks/:id
**Components**:
- components/tasks/TaskAssignee.tsx
- components/tasks/TaskDueDate.tsx
 
#### Task Filtering - MEDIUM
**Priority**: MEDIUM
**User Stories**:
- As a user, I can filter tasks by assignee
- As a user, I can filter tasks by priority
**Acceptance Criteria**:
- Filter by assignee (including "unassigned")
- Filter by priority level
- Clear filters button
**API Endpoints**:
- GET /api/workspaces/:workspaceId/tasks (with query params)
**Components**:
- components/tasks/TaskFilters.tsx
 
#### Real-time Task Updates - MEDIUM
**Priority**: MEDIUM - Enhances collaboration
**User Stories**:
- As a user, I see task updates immediately when others make changes
**Acceptance Criteria**:
- Task updates sync across all connected clients
- Activity feed shows recent changes
- Notifications for task assignments
**Implementation Notes**:
- Leverage SSE infrastructure from Workspaces domain
- Add activity tracking to task operations
- Broadcast changes on task mutations
**Components**:
- components/tasks/RealtimeBadge.tsx
- hooks/useTaskUpdates.ts
 
## Non-Functional Requirements
 
### Performance
- Initial page load < 2 seconds
- API response time < 200ms (p95)
- Support 100+ concurrent users
 
### Security
- All API routes protected except /api/auth/*
- SQL injection prevention (Prisma handles this)
- XSS prevention (React handles this)
- CSRF protection (Next.js built-in)
- Rate limiting on auth endpoints
 
### Accessibility
- WCAG 2.1 AA compliant
- Keyboard navigation support
- Screen reader support
- High contrast mode support
 
### Code Quality
- TypeScript strict mode
- 80%+ test coverage
- No console.log in production
- All components have TypeScript interfaces
 
## Database Schema
 
```prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  name      String
  avatar    String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  ownedWorkspaces   Workspace[] @relation("WorkspaceOwner")
  workspaceMembers  WorkspaceMember[]
  assignedTasks     Task[]      @relation("TaskAssignee")
  createdTasks      Task[]      @relation("TaskCreator")
  activities        Activity[]
}
 
model Workspace {
  id          String   @id @default(cuid())
  name        String
  description String?
  ownerId     String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
 
  owner     User              @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: Cascade)
  members   WorkspaceMember[]
  tasks     Task[]
  activities Activity[]
}
 
model WorkspaceMember {
  id         String   @id @default(cuid())
  workspaceId String
  userId      String
  role       Role     @default(MEMBER)
  joinedAt   DateTime @default(now())
 
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@unique([workspaceId, userId])
}
 
model Task {
  id          String     @id @default(cuid())
  title       String
  description String?
  status      TaskStatus @default(TODO)
  priority    Priority   @default(MEDIUM)
  dueDate     DateTime?
  workspaceId String
  assigneeId  String?
  creatorId   String
  createdAt   DateTime   @default(now())
  updatedAt   DateTime   @updatedAt
 
  workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
  assignee  User?     @relation("TaskAssignee", fields: [assigneeId], references: [id])
  creator   User      @relation("TaskCreator", fields: [creatorId], references: [id])
  activities Activity[]
}
 
model Activity {
  id         String   @id @default(cuid())
  type       ActivityType
  message    String
  taskId     String?
  workspaceId String
  userId     String
  createdAt  DateTime @default(now())
 
  task      Task?      @relation(fields: [taskId], references: [id], onDelete: Cascade)
  workspace Workspace  @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
  user      User       @relation(fields: [userId], references: [id])
}
 
enum Role {
  OWNER
  ADMIN
  MEMBER
  VIEWER
}
 
enum TaskStatus {
  TODO
  IN_PROGRESS
  DONE
}
 
enum Priority {
  LOW
  MEDIUM
  HIGH
  URGENT
}
 
enum ActivityType {
  TASK_CREATED
  TASK_UPDATED
  TASK_DELETED
  TASK_ASSIGNED
  STATUS_CHANGED
  MEMBER_JOINED
  MEMBER_LEFT
}

Success Criteria

  1. All CRITICAL and HIGH priority features implemented
  2. All quality gates passing (TypeScript, tests, coverage, lint, security)
  3. Test coverage ≥ 80%
  4. E2E tests for critical user flows
  5. Responsive design (mobile, tablet, desktop)
  6. Accessibility audit passed

Notes

  • Start with authentication - it's the foundation
  • Build workspace management before tasks (tasks need workspaces)
  • Real-time updates can be added after core features work
  • Use shadcn/ui for consistent, accessible components
  • Focus on getting one feature fully done before starting the next
 
---
 
## Project Structure
 
### Initial State (Before agentful)

taskflow/ ├── node_modules/ ├── package.json ├── tsconfig.json └── next.config.js

 
### Final State (After agentful)
 
**Hierarchical PRODUCT.md Structure**:

taskflow/ ├── .claude/ │ ├── agents/ │ ├── commands/ │ ├── skills/ │ └── product/ │ ├── overview.md # Project overview, tech stack │ ├── domains/ # HIERARCHICAL: Organized by domain │ │ ├── authentication/ │ │ │ ├── index.md # Auth domain overview │ │ │ ├── registration.md │ │ │ ├── login.md │ │ │ ├── logout.md │ │ │ └── profile.md │ │ ├── workspaces/ │ │ │ ├── index.md # Workspaces domain overview │ │ │ ├── create-workspace.md │ │ │ ├── view-workspaces.md │ │ │ ├── manage-members.md │ │ │ └── real-time-presence.md │ │ └── tasks/ │ │ ├── index.md # Tasks domain overview │ │ ├── create-task.md │ │ ├── view-tasks.md │ │ ├── update-status.md │ │ ├── assign-tasks.md │ │ ├── filter-tasks.md │ │ └── real-time-updates.md │ ├── non-functional.md # Performance, security, etc. │ └── database-schema.md # Prisma schema ├── .agentful/ │ ├── state.json │ ├── completion.json │ └── decisions.json ├── CLAUDE.md ├── prisma/ │ ├── schema.prisma │ └── migrations/ │ └── 20240118_init/ │ └── migration.sql ├── src/ │ ├── app/ │ │ ├── (auth)/ │ │ │ ├── login/ │ │ │ │ └── page.tsx │ │ │ └── register/ │ │ │ └── page.tsx │ │ ├── (dashboard)/ │ │ │ ├── workspace/ │ │ │ │ └── [id]/ │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── api/ │ │ │ ├── auth/ │ │ │ │ ├── login/route.ts │ │ │ │ ├── register/route.ts │ │ │ │ ├── logout/route.ts │ │ │ │ └── me/route.ts │ │ │ ├── workspaces/ │ │ │ │ ├── route.ts │ │ │ │ ├── [id]/ │ │ │ │ │ ├── route.ts │ │ │ │ │ ├── tasks/ │ │ │ │ │ │ ├── route.ts │ │ │ │ │ │ └── [taskId]/route.ts │ │ │ │ │ └── members/ │ │ │ │ │ └── route.ts │ │ │ │ └── events/ │ │ │ │ └── route.ts │ │ │ └── users/ │ │ │ └── me/ │ │ │ └── route.ts │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components/ │ │ ├── ui/ │ │ │ ├── button.tsx │ │ │ ├── input.tsx │ │ │ ├── dialog.tsx │ │ │ ├── form.tsx │ │ │ └── ... │ │ ├── auth/ │ │ │ ├── RegisterForm.tsx │ │ │ ├── LoginForm.tsx │ │ │ └── LogoutButton.tsx │ │ ├── workspaces/ │ │ │ ├── WorkspaceList.tsx │ │ │ ├── WorkspaceCard.tsx │ │ │ ├── CreateWorkspaceDialog.tsx │ │ │ ├── MemberList.tsx │ │ │ ├── InviteMemberDialog.tsx │ │ │ └── OnlineUsers.tsx │ │ ├── tasks/ │ │ │ ├── TaskBoard.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── CreateTaskDialog.tsx │ │ │ ├── TaskFilters.tsx │ │ │ ├── TaskAssignee.tsx │ │ │ ├── TaskDueDate.tsx │ │ │ └── RealtimeBadge.tsx │ │ └── layout/ │ │ ├── Header.tsx │ │ └── Sidebar.tsx │ ├── lib/ │ │ ├── auth.ts │ │ ├── prisma.ts │ │ └── utils.ts │ ├── hooks/ │ │ ├── useAuth.ts │ │ ├── useWorkspaces.ts │ │ ├── useTasks.ts │ │ ├── useTaskUpdates.ts │ │ └── useWorkspacePresence.ts │ ├── store/ │ │ └── authStore.ts │ ├── types/ │ │ └── index.ts │ └── styles/ │ └── globals.css ├── tests/ │ ├── unit/ │ │ ├── services/ │ │ │ └── auth.service.test.ts │ │ └── hooks/ │ │ └── useAuth.test.ts │ ├── integration/ │ │ └── api/ │ │ └── auth.test.ts │ └── e2e/ │ ├── auth.spec.ts │ └── tasks.spec.ts ├── .env.example ├── .env.local └── package.json

 
**Why Hierarchical?**
For production apps, organizing features by domain (authentication, workspaces, tasks) is more realistic than a flat PRODUCT.md. This structure:
- Scales to dozens of features without becoming overwhelming
- Allows teams to work on different domains in parallel
- Makes it easier to find and update specific features
- Mirrors real-world project organization
 
**File Count**: 127 files created
**Lines of Code**: ~8,500 lines
**Test Coverage**: 84%
 
---
 
## Implementation Walkthrough
 
<Tabs defaultValue="phase1" className="w-full">
 
<TabsContent value="phase1">
 
### Phase 1: Project Setup (15 minutes)
 
#### 1. Initialize Project
 
```bash
mkdir taskflow && cd taskflow
npm init -y
npm install next@14 react react-dom typescript @types/react @types/node
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

2. Initialize agentful

npx @itz4blitz/agentful init

3. Edit PRODUCT.md

Copy the complete PRODUCT.md from above into your project.

4. Start Development

claude
/agentful-start

agentful Output:

✅ Detected: Next.js 14 + TypeScript + Prisma + Tailwind
✅ Generated specialized agents
🎯 Starting Phase 1: Authentication (CRITICAL)
 
Delegating to @backend...

Phase 2: Database Schema (20 minutes)

Agent: @backend@architect

agentful Sequence:

@orchestrator: Creating database schema
→ @backend: Writing Prisma schema
→ @tester: Writing database tests
→ @reviewer: Validating schema

Generated Files:

prisma/schema.prisma

// This is your Prisma schema file
generator client {
  provider = "prisma-client-js"
}
 
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  name      String
  avatar    String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
 
  ownedWorkspaces  Workspace[]       @relation("WorkspaceOwner")
  workspaceMembers WorkspaceMember[]
  assignedTasks    Task[]            @relation("TaskAssignee")
  createdTasks     Task[]            @relation("TaskCreator")
  activities       Activity[]
}
 
// ... (rest of schema from PRODUCT.md)

prisma/migrations/20240118_init/migration.sql

-- CreateEnum
CREATE TYPE "Role" AS ENUM ('OWNER', 'ADMIN', 'MEMBER', 'VIEWER');
 
-- CreateEnum
CREATE TYPE "TaskStatus" AS ENUM ('TODO', 'IN_PROGRESS', 'DONE');
 
-- CreateEnum
CREATE TYPE "Priority" AS ENUM ('LOW', 'MEDIUM', 'HIGH', 'URGENT');
 
-- CreateEnum
CREATE TYPE "ActivityType" AS ENUM ('TASK_CREATED', 'TASK_UPDATED', 'TASK_DELETED', 'TASK_ASSIGNED', 'STATUS_CHANGED', 'MEMBER_JOINED', 'MEMBER_LEFT');
 
-- CreateTable
CREATE TABLE "User" (
    "id" TEXT NOT NULL,
    "email" TEXT NOT NULL,
    "password" TEXT NOT NULL,
    "name" TEXT NOT NULL,
    "avatar" TEXT,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,
 
    CONSTRAINT "User_pkey" PRIMARY KEY ("User")
);
 
-- CreateTable
CREATE TABLE "Workspace" (
    "id" TEXT NOT NULL,
    "name" TEXT NOT NULL,
    "description" TEXT,
    "ownerId" TEXT NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,
 
    CONSTRAINT "Workspace_pkey" PRIMARY KEY ("Workspace")
);
 
-- ... (rest of migrations)

Commands Run:

npx prisma migrate dev --name init
npx prisma generate

Validation: ✅ Schema matches PRODUCT.md requirements


Phase 3: Authentication System (1.5 hours)

Agent: @backend@frontend@tester

3.1 Backend Implementation

agentful Sequence:

@orchestrator: Building authentication feature
→ @backend: Creating auth service
→ @backend: Creating auth repositories
→ @backend: Creating auth API routes
→ @tester: Writing unit tests
→ @tester: Writing integration tests
→ @reviewer: Code review

Generated Files:

src/lib/auth.ts
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';
import { prisma } from './prisma';
 
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
 
export async function createToken(payload: any) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .sign(secret);
}
 
export async function verifyToken(token: string) {
  try {
    const { payload } = await jwtVerify(token, secret);
    return payload;
  } catch (error) {
    return null;
  }
}
 
export async function getSession() {
  const cookieStore = await cookies();
  const token = cookieStore.get('token')?.value;
 
  if (!token) return null;
 
  const payload = await verifyToken(token);
  if (!payload) return null;
 
  const user = await prisma.user.findUnique({
    where: { id: payload.userId as string },
    select: {
      id: true,
      email: true,
      name: true,
      avatar: true,
    },
  });
 
  return user;
}
 
export async function hashPassword(password: string): Promise<string> {
  const bcrypt = require('bcrypt');
  return bcrypt.hash(password, 10);
}
 
export async function comparePassword(
  password: string,
  hashedPassword: string
): Promise<boolean> {
  const bcrypt = require('bcrypt');
  return bcrypt.compare(password, hashedPassword);
}
src/app/api/auth/register/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { hashPassword, createToken } from '@/lib/auth';
import { z } from 'zod';
 
const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2),
});
 
export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const validated = registerSchema.parse(body);
 
    // Check if user exists
    const existing = await prisma.user.findUnique({
      where: { email: validated.email },
    });
 
    if (existing) {
      return NextResponse.json(
        { error: 'User already exists' },
        { status: 409 }
      );
    }
 
    // Create user
    const hashedPassword = await hashPassword(validated.password);
    const user = await prisma.user.create({
      data: {
        email: validated.email,
        password: hashedPassword,
        name: validated.name,
      },
      select: {
        id: true,
        email: true,
        name: true,
        avatar: true,
      },
    });
 
    // Create token
    const token = await createToken({ userId: user.id });
 
    // Set cookie
    const response = NextResponse.json(user, { status: 201 });
    response.cookies.set('token', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 7, // 7 days
    });
 
    return response;
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation failed', details: error.errors },
        { status: 400 }
      );
    }
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}
src/app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { comparePassword, createToken } from '@/lib/auth';
import { z } from 'zod';
 
const loginSchema = z.object({
  email: z.string().email(),
  password: z.string(),
});
 
export async function POST(req: NextRequest) {
  try {
    const body = await req.json();
    const validated = loginSchema.parse(body);
 
    // Find user
    const user = await prisma.user.findUnique({
      where: { email: validated.email },
    });
 
    if (!user) {
      return NextResponse.json(
        { error: 'Invalid credentials' },
        { status: 401 }
      );
    }
 
    // Verify password
    const isValid = await comparePassword(validated.password, user.password);
    if (!isValid) {
      return NextResponse.json(
        { error: 'Invalid credentials' },
        { status: 401 }
      );
    }
 
    // Create token
    const token = await createToken({ userId: user.id });
 
    // Set cookie
    const response = NextResponse.json({
      id: user.id,
      email: user.email,
      name: user.name,
      avatar: user.avatar,
    });
    response.cookies.set('token', token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: 60 * 60 * 24 * 7, // 7 days
    });
 
    return response;
  } catch (error) {
    if (error instanceof z.ZodError) {
      return NextResponse.json(
        { error: 'Validation failed', details: error.errors },
        { status: 400 }
      );
    }
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

3.2 Frontend Implementation

Agent: @frontend

src/hooks/useAuth.ts
'use client';
 
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
 
interface User {
  id: string;
  email: string;
  name: string;
  avatar?: string;
}
 
interface AuthState {
  user: User | null;
  isLoading: boolean;
  isAuthenticated: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (email: string, password: string, name: string) => Promise<void>;
  logout: () => Promise<void>;
  fetchSession: () => Promise<void>;
}
 
export const useAuth = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      isLoading: true,
      isAuthenticated: false,
 
      login: async (email: string, password: string) => {
        const res = await fetch('/api/auth/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email, password }),
        });
 
        if (!res.ok) {
          const error = await res.json();
          throw new Error(error.error || 'Login failed');
        }
 
        const user = await res.json();
        set({ user, isAuthenticated: true, isLoading: false });
      },
 
      register: async (email: string, password: string, name: string) => {
        const res = await fetch('/api/auth/register', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email, password, name }),
        });
 
        if (!res.ok) {
          const error = await res.json();
          throw new Error(error.error || 'Registration failed');
        }
 
        const user = await res.json();
        set({ user, isAuthenticated: true, isLoading: false });
      },
 
      logout: async () => {
        await fetch('/api/auth/logout', { method: 'POST' });
        set({ user: null, isAuthenticated: false, isLoading: false });
      },
 
      fetchSession: async () => {
        try {
          const res = await fetch('/api/auth/me');
          if (res.ok) {
            const user = await res.json();
            set({ user, isAuthenticated: true, isLoading: false });
          } else {
            set({ user: null, isAuthenticated: false, isLoading: false });
          }
        } catch (error) {
          set({ user: null, isAuthenticated: false, isLoading: false });
        }
      },
    }),
    {
      name: 'auth-storage',
    }
  )
);
src/app/(auth)/login/page.tsx
'use client';
 
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/hooks/useAuth';
 
export default function LoginPage() {
  const router = useRouter();
  const { login } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    setIsLoading(true);
 
    try {
      await login(email, password);
      router.push('/workspace');
    } catch (err: any) {
      setError(err.message);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <Card className="w-full max-w-md">
        <CardHeader>
          <CardTitle>Welcome Back</CardTitle>
          <CardDescription>Sign in to your TaskFlow account</CardDescription>
        </CardHeader>
        <CardContent>
          <form onSubmit={handleSubmit} className="space-y-4">
            {error && (
              <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
                {error}
              </div>
            )}
 
            <div>
              <label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
                Email
              </label>
              <Input
                id="email"
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="you@example.com"
                required
              />
            </div>
 
            <div>
              <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
                Password
              </label>
              <Input
                id="password"
                type="password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                placeholder="••••••••"
                required
              />
            </div>
 
            <Button type="submit" className="w-full" isLoading={isLoading}>
              Sign In
            </Button>
 
            <p className="text-center text-sm text-gray-600">
              Don't have an account?{' '}
              <a href="/register" className="text-blue-600 hover:underline">
                Sign up
              </a>
            </p>
          </form>
        </CardContent>
      </Card>
    </div>
  );
}

3.3 Tests

Agent: @tester

tests/integration/api/auth/login.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { prisma } from '@/lib/prisma';
import { hashPassword } from '@/lib/auth';
 
describe('POST /api/auth/login', () => {
  let testUserId: string;
 
  beforeAll(async () => {
    // Create test user
    const hashedPassword = await hashPassword('password123');
    const user = await prisma.user.create({
      data: {
        email: 'test@example.com',
        password: hashedPassword,
        name: 'Test User',
      },
    });
    testUserId = user.id;
  });
 
  afterAll(async () => {
    // Cleanup
    await prisma.user.delete({ where: { id: testUserId } });
  });
 
  it('should login with valid credentials', async () => {
    const res = await fetch('http://localhost:3000/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'test@example.com',
        password: 'password123',
      }),
    });
 
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data).toHaveProperty('email', 'test@example.com');
    expect(data).toHaveProperty('name', 'Test User');
    expect(res.headers.get('set-cookie')).toContain('token=');
  });
 
  it('should reject invalid credentials', async () => {
    const res = await fetch('http://localhost:3000/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: 'test@example.com',
        password: 'wrongpassword',
      }),
    });
 
    expect(res.status).toBe(401);
    const data = await res.json();
    expect(data).toHaveProperty('error');
  });
 
  it('should validate required fields', async () => {
    const res = await fetch('http://localhost:3000/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'test@example.com' }),
    });
 
    expect(res.status).toBe(400);
  });
});

Phase 4: Workspace Management (1.5 hours)

Agent: @backend@frontend@tester

Similar authentication flow applied to workspaces. Key files created:

Backend Files

  • src/app/api/workspaces/route.ts - List/create workspaces
  • src/app/api/workspaces/[id]/route.ts - Get/update/delete workspace
  • src/app/api/workspaces/[id]/members/route.ts - Manage members

Frontend Files

  • src/components/workspaces/WorkspaceList.tsx
  • src/components/workspaces/CreateWorkspaceDialog.tsx
  • src/hooks/useWorkspaces.ts

Tests

  • __tests__/unit/services/workspace.service.test.ts
  • __tests__/integration/api/workspaces.test.ts

Phase 5: Task Management (2 hours)

Agent: @backend@frontend@tester

Key Implementation: Task Board with Drag-and-Drop

src/components/tasks/TaskBoard.tsx
'use client';
 
import { useState } from 'react';
import { useTasks } from '@/hooks/useTasks';
import { TaskCard } from './TaskCard';
import { CreateTaskDialog } from './CreateTaskDialog';
 
interface TaskBoardProps {
  workspaceId: string;
}
 
type TaskStatus = 'TODO' | 'IN_PROGRESS' | 'DONE';
 
export function TaskBoard({ workspaceId }: TaskBoardProps) {
  const { tasks, isLoading, updateTaskStatus } = useTasks(workspaceId);
  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
 
  const columns = [
    { id: 'TODO', title: 'To Do', color: 'bg-gray-100' },
    { id: 'IN_PROGRESS', title: 'In Progress', color: 'bg-blue-50' },
    { id: 'DONE', title: 'Done', color: 'bg-green-50' },
  ] as const;
 
  const handleDrop = async (taskId: string, newStatus: TaskStatus) => {
    await updateTaskStatus(taskId, newStatus);
  };
 
  if (isLoading) {
    return <div>Loading tasks...</div>;
  }
 
  return (
    <div className="p-6">
      <div className="flex justify-between items-center mb-6">
        <h2 className="text-2xl font-bold">Task Board</h2>
        <Button onClick={() => setIsCreateDialogOpen(true)}>
          Create Task
        </Button>
      </div>
 
      <div className="grid grid-cols-3 gap-6">
        {columns.map((column) => (
          <div key={column.id} className={`${column.color} rounded-lg p-4`}>
            <h3 className="font-semibold text-lg mb-4">{column.title}</h3>
            <div className="space-y-3">
              {tasks
                .filter((task) => task.status === column.id)
                .map((task) => (
                  <TaskCard
                    key={task.id}
                    task={task}
                    onStatusChange={(newStatus) => handleDrop(task.id, newStatus)}
                  />
                ))}
            </div>
          </div>
        ))}
      </div>
 
      {isCreateDialogOpen && (
        <CreateTaskDialog
          workspaceId={workspaceId}
          onClose={() => setIsCreateDialogOpen(false)}
        />
      )}
    </div>
  );
}

Phase 6: Real-time Updates (1 hour)

Agent: @backend@frontend

Implementation: Server-Sent Events

src/app/api/workspaces/[id]/events/route.ts
import { NextRequest } from 'next/server';
import { prisma } from '@/lib/prisma';
 
export async function GET(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  const workspaceId = params.id;
 
  const encoder = new TextEncoder();
 
  const stream = new ReadableStream({
    async start(controller) {
      // Send initial connection message
      controller.enqueue(
        encoder.encode(`data: ${JSON.stringify({ type: 'connected' })}\n\n`)
      );
 
      // Poll for updates (every 2 seconds)
      const interval = setInterval(async () => {
        const recentTasks = await prisma.task.findMany({
          where: {
            workspaceId,
            updatedAt: {
              gte: new Date(Date.now() - 2000),
            },
          },
        });
 
        if (recentTasks.length > 0) {
          controller.enqueue(
            encoder.encode(
              `data: ${JSON.stringify({ type: 'tasks_updated', data: recentTasks })}\n\n`
            )
          );
        }
      }, 2000);
 
      // Cleanup on close
      req.signal.addEventListener('abort', () => {
        clearInterval(interval);
        controller.close();
      });
    },
  });
 
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  });
}
src/hooks/useTaskUpdates.ts
'use client';
 
import { useEffect, useState } from 'react';
 
interface TaskUpdate {
  type: string;
  data?: any;
}
 
export function useTaskUpdates(workspaceId: string) {
  const [updates, setUpdates] = useState<TaskUpdate[]>([]);
 
  useEffect(() => {
    const eventSource = new EventSource(
      `/api/workspaces/${workspaceId}/events`
    );
 
    eventSource.onmessage = (event) => {
      const update = JSON.parse(event.data);
      setUpdates((prev) => [...prev, update]);
    };
 
    return () => {
      eventSource.close();
    };
  }, [workspaceId]);
 
  return updates;
}

Results

Time Breakdown

PhaseDurationAgent
Project Setup15 min@orchestrator
Database Schema20 min@backend + @architect
Authentication1.5 hours@backend + @frontend + @tester
Workspace Management1.5 hours@backend + @frontend + @tester
Task Management2 hours@backend + @frontend + @tester
Real-time Updates1 hour@backend + @frontend
Total6.5 hours

Code Metrics

File Count: 127 files
- Backend files: 42
- Frontend files: 58
- Test files: 27
 
Lines of Code: ~8,500 lines
- Application code: ~6,200
- Test code: ~2,300
 
Test Coverage: 84%
- Unit tests: 89% coverage
- Integration tests: 82% coverage
- E2E tests: 5 critical flows covered
 
Quality Gates Status:
✅ All tests passing (245 tests)
✅ No type errors (adapts to stack)
✅ No lint errors
✅ Coverage threshold met (84% ≥ 80%)
✅ No dead code
✅ No security vulnerabilities

Before vs After

Before (Empty Project)

taskflow/
├── package.json
└── tsconfig.json

Capabilities: None Features: 0/5 implemented Tests: 0

After (Completed Application)

taskflow/
├── 127 files
├── 8,500+ lines of code
├── 245 passing tests
└── 84% coverage

Capabilities:

  • ✅ User registration & login
  • ✅ Team workspace creation
  • ✅ Task management with drag-and-drop
  • ✅ Real-time collaboration
  • ✅ Role-based permissions

Features: 5/5 implemented (100%) Tests: 245 tests passing


Agent Delegation Sequence

High-Level Flow

User: /agentful-start
 
@orchestrator
  ├─ Analyze PRODUCT.md
  ├─ Detect tech stack
  ├─ Prioritize features
  └─ Start Phase 1: Authentication
 
      @backend
        ├─ Create Prisma schema
        ├─ Implement auth service
        ├─ Create API routes
        └─ Report completion
 
      @frontend
        ├─ Create useAuth hook
        ├─ Build login page
        ├─ Build register page
        └─ Report completion
 
      @tester
        ├─ Write unit tests
        ├─ Write integration tests
        ├─ Run tests
        └─ Verify 80% coverage
 
      @reviewer
        ├─ Check code quality
        ├─ Check for dead code
        ├─ Run security scan
        └─ Approve phase
 
@orchestrator
  ├─ Mark authentication complete
  ├─ Update progress
  └─ Start Phase 2: Workspaces
 
      [Repeat pattern for each feature]
 
@orchestrator
  ├─ All features complete
  ├─ Run final validation
  └─ Report success
 
@fixer (if validation fails)
  ├─ Identify issues
  ├─ Fix errors
  └─ Re-validate

Key Decisions Made

During development, agentful encountered these decisions and resolved them:

Decision 1: Auth Library

Question: Which auth library to use? Options: NextAuth.js, Lucia, Custom JWT Decision: Custom JWT implementation Reasoning: More control, simpler for this use case, fewer dependencies

Decision 2: Real-time Approach

Question: SSE vs WebSockets vs Polling? Options: Pusher, WebSockets, SSE, Polling Decision: Server-Sent Events Reasoning: Built into Next.js, simpler than WebSockets, unidirectional flow sufficient

Decision 3: State Management

Question: Which state management library? Options: Redux, Zustand, Context API Decision: Zustand Reasoning: Simpler than Redux, more powerful than Context, great TypeScript support

Decision 4: Component Library

Question: Build from scratch or use library? Options: Material UI, Chakra UI, shadcn/ui, Custom Decision: shadcn/ui Reasoning: Copy-paste components, full ownership, Tailwind-based, accessible


Validation Results

Quality Gates Run 1 (After Phase 3)

Running validation...
 
✅ TypeScript: No errors
✅ Tests: 78/78 passing
✅ Coverage: 82% (threshold: 80%)
⚠️  Lint: 3 warnings
   - Unused import in AuthForm.tsx
   - Missing return type in auth.ts
   - Unused variable in login/page.tsx
✅ Dead Code: None found
✅ Security: No vulnerabilities
 
Status: PASSED (with warnings)

Quality Gates Run 2 (Final)

Running final validation...
 
✅ TypeScript: No errors
✅ Tests: 245/245 passing
✅ Coverage: 84% (threshold: 80%)
✅ Lint: No errors or warnings
✅ Dead Code: None found
✅ Security: No vulnerabilities
✅ E2E Tests: 5/5 flows passing
 
Status: ALL GATES PASSED 🎉

Lessons Learned

What Went Well

  1. Feature Prioritization - Starting with authentication was the right choice
  2. Incremental Development - One feature at a time prevented overwhelm
  3. Test-Driven Approach - Writing tests alongside code caught issues early
  4. Type Safety - TypeScript prevented many runtime errors

Challenges Encountered

  1. SSE Implementation - Required experimentation with Next.js streaming
  2. Drag-and-Drop - Initial implementation had performance issues, refactored to use simpler approach
  3. Real-time Testing - Required test setup adjustments for SSE

Improvements Made

  1. Added request validation after discovering missing input sanitization
  2. Refactored auth service to separate concerns better
  3. Optimized database queries after noticing N+1 issues
  4. Added error boundaries after catching React errors

Next Steps for This Project

Potential Enhancements

  1. Email Notifications - Notify users of task assignments
  2. File Attachments - Attach files to tasks
  3. Comments - Comment threads on tasks
  4. Search & Filters - Advanced task search
  5. Analytics Dashboard - Team productivity insights

Scaling Considerations

  1. Caching - Add Redis for session and data caching
  2. CDN - Serve static assets via CDN
  3. Database Optimization - Add indexes for common queries
  4. Rate Limiting - Implement stricter rate limiting
  5. Monitoring - Add error tracking (Sentry) and monitoring

Conclusion

This full-stack SaaS application demonstrates agentful's ability to:

  • Build complex features autonomously
  • Maintain high code quality standards
  • Coordinate multiple specialized agents
  • Deliver working software in hours, not weeks

The 6.5-hour development time includes:

  • Complete authentication system
  • Team collaboration features
  • Real-time updates
  • Comprehensive test suite
  • Production-ready code

Compare to traditional development: 2-3 weeks

Time saved: ~85%


Try It Yourself

Want to build this yourself?

# 1. Create project
mkdir taskflow && cd taskflow
npm init -y
 
# 2. Initialize agentful
npx @itz4blitz/agentful init
 
# 3. Copy PRODUCT.md (from this example)
 
# 4. Start building
claude
/agentful-start
 
# That's it! Come back in 6-8 hours to a working app.

Previous: Examples Index Next: API Development Example