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
- All CRITICAL and HIGH priority features implemented
- All quality gates passing (TypeScript, tests, coverage, lint, security)
- Test coverage ≥ 80%
- E2E tests for critical user flows
- Responsive design (mobile, tablet, desktop)
- 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 -p2. Initialize agentful
npx @itz4blitz/agentful init3. Edit PRODUCT.md
Copy the complete PRODUCT.md from above into your project.
4. Start Development
claude
/agentful-startagentful 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 schemaGenerated 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 generateValidation: ✅ 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 reviewGenerated 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 workspacessrc/app/api/workspaces/[id]/route.ts- Get/update/delete workspacesrc/app/api/workspaces/[id]/members/route.ts- Manage members
Frontend Files
src/components/workspaces/WorkspaceList.tsxsrc/components/workspaces/CreateWorkspaceDialog.tsxsrc/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
| Phase | Duration | Agent |
|---|---|---|
| Project Setup | 15 min | @orchestrator |
| Database Schema | 20 min | @backend + @architect |
| Authentication | 1.5 hours | @backend + @frontend + @tester |
| Workspace Management | 1.5 hours | @backend + @frontend + @tester |
| Task Management | 2 hours | @backend + @frontend + @tester |
| Real-time Updates | 1 hour | @backend + @frontend |
| Total | 6.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 vulnerabilitiesBefore vs After
Before (Empty Project)
taskflow/
├── package.json
└── tsconfig.jsonCapabilities: None Features: 0/5 implemented Tests: 0
After (Completed Application)
taskflow/
├── 127 files
├── 8,500+ lines of code
├── 245 passing tests
└── 84% coverageCapabilities:
- ✅ 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-validateKey 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
- Feature Prioritization - Starting with authentication was the right choice
- Incremental Development - One feature at a time prevented overwhelm
- Test-Driven Approach - Writing tests alongside code caught issues early
- Type Safety - TypeScript prevented many runtime errors
Challenges Encountered
- SSE Implementation - Required experimentation with Next.js streaming
- Drag-and-Drop - Initial implementation had performance issues, refactored to use simpler approach
- Real-time Testing - Required test setup adjustments for SSE
Improvements Made
- Added request validation after discovering missing input sanitization
- Refactored auth service to separate concerns better
- Optimized database queries after noticing N+1 issues
- Added error boundaries after catching React errors
Next Steps for This Project
Potential Enhancements
- Email Notifications - Notify users of task assignments
- File Attachments - Attach files to tasks
- Comments - Comment threads on tasks
- Search & Filters - Advanced task search
- Analytics Dashboard - Team productivity insights
Scaling Considerations
- Caching - Add Redis for session and data caching
- CDN - Serve static assets via CDN
- Database Optimization - Add indexes for common queries
- Rate Limiting - Implement stricter rate limiting
- 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