Skip to content
Lesson 16 of 22

Project: Full-Stack SaaS App

15 min read

Project Overview

This is the capstone project of Module IV. We are building FlowTask --- not just a landing page this time, but the full product. A task management SaaS application with user authentication, a database, projects, tasks with statuses and priorities, a real-time dashboard, and all the CRUD operations that make a product actually work.

This lesson combines everything from the entire bootcamp: prompt engineering, CLAUDE.md configuration, Git workflows, frontend components, backend API routes, database design, authentication, and testing. If you can build this, you can build anything.

What We Are Building

  • Authentication: Sign in with GitHub, protected routes, session management
  • Projects: Create, read, update, and delete projects
  • Tasks: Full task management within projects --- create, edit, assign, change status, set priorities and due dates
  • Dashboard: Stats cards, activity charts, and a recent activity feed
  • Settings: User profile management

Tech Stack

  • Next.js 14 with App Router
  • Prisma as the ORM
  • SQLite for development (easy to switch to PostgreSQL for production)
  • NextAuth.js for authentication
  • Tailwind CSS for styling

Setting Up the Project

Before writing any code, we need a proper foundation. Start by creating the project CLAUDE.md so the AI understands the context for every prompt in this session.

The Project CLAUDE.md

Create a .claude/CLAUDE.md file in your project root:

# FlowTask SaaS Application

## Tech Stack
- Next.js 14 (App Router)
- Prisma with SQLite (dev) / PostgreSQL (prod)
- NextAuth.js with GitHub provider
- Tailwind CSS
- TypeScript (strict mode)

## Project Structure
- /app — Next.js App Router pages
- /app/(marketing) — Public pages (landing, pricing)
- /app/(app) — Authenticated app pages (dashboard, projects, tasks)
- /components — Reusable React components
- /lib — Utility functions, database client, auth config
- /prisma — Schema and migrations

## Conventions
- Use Server Components by default, Client Components only when needed
- Use Server Actions for mutations
- All API routes under /app/api/
- Tailwind for all styling, no CSS modules
- Commit messages: conventional commits (feat:, fix:, refactor:)

## Current State
- Phase 1: Project scaffolding and database schema

This file is your project's brain. Update the "Current State" section as you build each feature. The AI reads this before every prompt, keeping all responses consistent with your architecture.

The Initial Scaffold Prompt

Create a Next.js 14 project with TypeScript and Tailwind CSS. Set up the
following structure:

- App Router with two route groups: (marketing) for public pages and (app)
  for authenticated pages
- Prisma with SQLite database
- NextAuth.js configured but not yet connected to a provider
- A basic layout for the (app) route group with a sidebar navigation
- A placeholder dashboard page at /dashboard

Install all dependencies and configure tsconfig for strict mode.

After the AI generates the scaffold, verify it works:

npm run dev

Visit http://localhost:3000 and confirm you see the placeholder dashboard. Then commit:

git add .
git commit -m "feat: initial project scaffold with Next.js, Prisma, NextAuth"

Database Schema Design

The database is the backbone of any SaaS application. We need three core tables: users, projects, and tasks.

The Prompt

Create a Prisma schema with these models:

User:
- id (cuid, primary key)
- name (string, optional)
- email (string, unique)
- emailVerified (datetime, optional)
- image (string, optional)
- accounts (relation to Account model for NextAuth)
- sessions (relation to Session model for NextAuth)
- projects (relation to Project[])
- createdAt, updatedAt

Project:
- id (cuid, primary key)
- name (string)
- description (string, optional)
- color (string, default "#6366f1")
- userId (string, foreign key to User)
- tasks (relation to Task[])
- createdAt, updatedAt

Task:
- id (cuid, primary key)
- title (string)
- description (string, optional)
- status (enum: TODO, IN_PROGRESS, DONE, default TODO)
- priority (enum: LOW, MEDIUM, HIGH, URGENT, default MEDIUM)
- dueDate (datetime, optional)
- projectId (string, foreign key to Project)
- assigneeId (string, optional, foreign key to User)
- createdAt, updatedAt

Include the NextAuth required models (Account, Session, VerificationToken).
Add appropriate indexes on foreign keys and frequently queried fields.

Running Migrations

After the AI generates the schema, apply it:

npx prisma migrate dev --name init

This creates the SQLite database file and applies the schema. You can inspect the database with:

npx prisma studio

Prisma Studio opens a visual database browser at http://localhost:5555. Bookmark this --- it is incredibly useful for debugging data issues during development.

The Prisma Client

The AI should also create a singleton Prisma client:

// lib/prisma.ts
import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined;
};

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

This singleton pattern prevents creating multiple database connections during development when Next.js hot-reloads.

Authentication Flow

Authentication is non-negotiable for a SaaS app. NextAuth.js makes it straightforward.

The Prompt

Set up NextAuth.js with the GitHub provider. Configure:

1. The auth configuration in lib/auth.ts using the Prisma adapter
2. The API route handler at app/api/auth/[...nextauth]/route.ts
3. A sign-in page at app/(marketing)/login/page.tsx with a "Sign in with
   GitHub" button
4. A sign-out button component
5. Middleware that protects all /dashboard and /projects routes — redirect
   to /login if not authenticated
6. A getCurrentUser helper function that gets the session user with their
   database record

Use environment variables for GITHUB_ID, GITHUB_SECRET, NEXTAUTH_SECRET,
and NEXTAUTH_URL.

Environment Variables

Create a .env.local file (never commit this):

GITHUB_ID=your_github_oauth_app_id
GITHUB_SECRET=your_github_oauth_app_secret
NEXTAUTH_SECRET=generate_with_openssl_rand_base64_32
NEXTAUTH_URL=http://localhost:3000

To get GitHub OAuth credentials, go to GitHub Settings, then Developer Settings, then OAuth Apps, and create a new app with the callback URL http://localhost:3000/api/auth/callback/github.

Protecting Routes

The middleware file is critical. It intercepts every request and checks authentication:

// middleware.ts
import { withAuth } from "next-auth/middleware";

export default withAuth({
  pages: {
    signIn: "/login",
  },
});

export const config = {
  matcher: ["/dashboard/:path*", "/projects/:path*", "/settings/:path*"],
};

Test the flow: visit /dashboard while logged out. You should be redirected to /login. Sign in with GitHub. You should land on /dashboard. This is the authentication guard working.

git add .
git commit -m "feat: add NextAuth.js with GitHub provider and route protection"

The Dashboard

The dashboard is the first thing users see after signing in. It needs to communicate activity and progress at a glance.

The Prompt

Build the dashboard page at /dashboard. It should include:

1. Welcome header: "Welcome back, {user.name}" with today's date
2. Stats cards row (4 cards):
   - Total Tasks (count of all user's tasks)
   - Completed Today (tasks marked DONE today)
   - In Progress (tasks with IN_PROGRESS status)
   - Overdue (tasks past due date that aren't DONE)
   Each card has an icon, the number in large text, and a label

3. A chart showing tasks completed over the last 7 days (bar chart or line
   chart using recharts library)

4. Recent Activity list showing the last 10 task updates with:
   - Task title
   - What changed (created, status changed, completed)
   - Timestamp (relative: "2 hours ago")
   - Project name

Fetch all data server-side using Prisma queries in the page component.
Use a grid layout: stats cards in a 4-column row, chart takes 2/3 width
with recent activity taking 1/3 on desktop.

Iterating on the Dashboard

The first version will be functional but probably needs polish. Common iterations:

The stats cards look flat. Add a subtle gradient background to each card
(indigo-50 to indigo-100 for the first, green-50 to green-100 for
completed, etc.). Add an arrow icon showing trend direction compared to
yesterday.
The chart looks basic. Add gridlines, softer colors, rounded bar corners,
and tooltips showing the exact count on hover. Use indigo-500 as the bar
color.

The dashboard is a living component. As you add more features, you will come back and add more widgets. For now, get the core layout right and commit.

CRUD Operations for Projects

CRUD stands for Create, Read, Update, Delete --- the four fundamental operations of any data-driven application.

The Prompt

Build the complete projects feature:

1. Projects list page at /projects showing all of the current user's
   projects in a grid of cards. Each card shows project name, description
   preview, task count, and a colored dot matching the project color.

2. Create project: a modal form triggered by a "New Project" button. Fields:
   name (required), description (optional), color picker. Use a Server Action
   to create the project in the database.

3. Project detail page at /projects/[id] showing the project header (name,
   description, edit button, delete button) and a list of all tasks in that
   project.

4. Edit project: clicking "Edit" opens a modal pre-filled with current
   values. Server Action to update.

5. Delete project: clicking "Delete" shows a confirmation dialog. "Are you
   sure? This will delete all tasks in this project." Server Action to
   delete the project and all associated tasks.

Use Server Actions for all mutations. Revalidate the path after each
mutation so the UI updates immediately.

Server Actions Pattern

The AI should generate Server Actions like this:

"use server";

import { prisma } from "@/lib/prisma";
import { getCurrentUser } from "@/lib/auth";
import { revalidatePath } from "next/cache";

export async function createProject(formData: FormData) {
  const user = await getCurrentUser();
  if (!user) throw new Error("Unauthorized");

  const name = formData.get("name") as string;
  const description = formData.get("description") as string;
  const color = formData.get("color") as string;

  await prisma.project.create({
    data: {
      name,
      description,
      color: color || "#6366f1",
      userId: user.id,
    },
  });

  revalidatePath("/projects");
}

This pattern --- authenticate, validate, mutate, revalidate --- repeats for every CRUD operation. Once the AI establishes it for projects, it will apply the same pattern to tasks.

CRUD Operations for Tasks

Tasks are the core of FlowTask. They need more features than projects because they are the primary object users interact with.

The Prompt

Build the complete tasks feature within projects:

1. Task creation: "Add Task" button within a project page opens an inline
   form (not a modal — it should feel quick). Fields: title (required),
   description (optional), status dropdown, priority dropdown, due date
   picker. Server Action to create.

2. Task list view within the project page: show tasks grouped by status
   (TODO, IN_PROGRESS, DONE). Each task shows title, priority badge
   (color-coded), due date (red if overdue), and an assignee avatar.

3. Task detail view: clicking a task opens a slide-over panel from the right
   showing all task fields, an edit form, and a comments section placeholder.

4. Inline editing: clicking a task's status badge cycles through statuses
   without opening the detail view. Use optimistic updates.

5. Drag and drop: tasks can be dragged between status columns in a kanban
   view. Add a toggle between "List view" and "Board view" at the top of
   the project page.

Use Server Actions for all mutations. Add loading states and optimistic
updates for a snappy feel.

The Kanban Board

The drag-and-drop kanban view is the most complex UI element. The AI will likely use a library like @hello-pangea/dnd or dnd-kit:

import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";

const onDragEnd = async (result: DropResult) => {
  if (!result.destination) return;

  const taskId = result.draggableId;
  const newStatus = result.destination.droppableId as TaskStatus;

  // Optimistic update
  setTasks((prev) =>
    prev.map((task) =>
      task.id === taskId ? { ...task, status: newStatus } : task
    )
  );

  // Server update
  await updateTaskStatus(taskId, newStatus);
};

If the kanban implementation is not working well, break it down:

The drag and drop isn't working smoothly. Let's simplify: use @hello-pangea/dnd
with three columns (TODO, IN_PROGRESS, DONE). Each column is a Droppable, each
task card is a Draggable. On drop, call the updateTaskStatus Server Action and
use optimistic updates.

Search and Filtering

Once users have dozens of tasks, they need ways to find them quickly.

The Prompt

Add search and filtering to the tasks list:

1. Search bar at the top of the project page that filters tasks by title
   (client-side filtering for instant results)

2. Filter dropdowns:
   - Status: All, TODO, IN_PROGRESS, DONE
   - Priority: All, LOW, MEDIUM, HIGH, URGENT
   - Due date: All, Overdue, Due Today, Due This Week, No Due Date

3. Sort options: Created (newest/oldest), Due Date (nearest/farthest),
   Priority (highest/lowest)

4. Show active filter count on a "Filters" button. Clicking "Clear All"
   resets all filters.

Store filter state in URL search params so filters survive page refresh.

Client-Side vs Server-Side

For this project, client-side filtering works fine because the task count per project is manageable. The AI should use useMemo to filter efficiently:

const filteredTasks = useMemo(() => {
  return tasks
    .filter((task) => {
      if (statusFilter && task.status !== statusFilter) return false;
      if (priorityFilter && task.priority !== priorityFilter) return false;
      if (searchQuery && !task.title.toLowerCase().includes(searchQuery.toLowerCase()))
        return false;
      return true;
    })
    .sort((a, b) => {
      if (sortBy === "dueDate") return compareDates(a.dueDate, b.dueDate);
      if (sortBy === "priority") return comparePriority(a.priority, b.priority);
      return compareCreatedAt(a.createdAt, b.createdAt);
    });
}, [tasks, statusFilter, priorityFilter, searchQuery, sortBy]);

For larger datasets, you would move filtering to the server with Prisma query conditions. The prompt would change to request a Server Component that accepts search params and queries the database with where clauses.

Settings Page

Every SaaS application needs a settings page where users can manage their account.

The Prompt

Create a settings page at /settings with these sections:

1. Profile: Display and edit name, email (read-only), and avatar. Use a
   form with Server Action to update the name.

2. Preferences: Toggle for email notifications (task assigned, task
   completed, weekly digest). Store in a UserPreferences model or JSON
   field.

3. Danger Zone: Account deletion. A red-bordered section with "Delete
   Account" button. Clicking it shows a confirmation that requires typing
   "DELETE" to confirm. Server Action deletes the user and all associated
   data, then signs them out.

Use a tab or sidebar navigation to switch between settings sections.

The account deletion confirmation is an important UX pattern. It prevents accidental data loss:

const [confirmText, setConfirmText] = useState("");

const canDelete = confirmText === "DELETE";

File Upload

Attaching files to tasks makes FlowTask more useful for real work.

The Prompt

Add file attachments to tasks:

1. Add an Attachment model to Prisma: id, filename, url, size, mimeType,
   taskId, uploadedBy, createdAt

2. File upload component: drag-and-drop zone + click to browse. Accept
   images, PDFs, and documents up to 10MB.

3. Store files locally in /public/uploads during development. Create an
   API route POST /api/upload that handles multipart form data.

4. Display attachments in the task detail view: show filename, size, upload
   date, and a download link. Images show a thumbnail preview.

5. Delete attachment button with confirmation.

Run the migration for the new Attachment model.

For production, you would replace local storage with S3 or a similar service. The prompt to make that switch later is straightforward:

Replace local file storage with S3-compatible storage using the @aws-sdk/
client-s3 package. Use environment variables for bucket name, region,
access key, and secret key. Update the upload API route to stream files
directly to S3 and store the S3 URL in the database.

The Full Prompt Sequence

Let us step back and look at the entire prompt sequence used to build FlowTask:

  1. Scaffold --- Project setup with Next.js, Prisma, NextAuth, Tailwind
  2. Schema --- Database models for User, Project, Task
  3. Auth --- GitHub OAuth, protected routes, session management
  4. Dashboard --- Stats cards, chart, recent activity
  5. Projects CRUD --- List, create, edit, delete projects
  6. Tasks CRUD --- List, create, edit, delete, status changes
  7. Kanban --- Drag-and-drop board view
  8. Search/Filter --- Client-side filtering with URL state
  9. Settings --- Profile, preferences, account deletion
  10. File Upload --- Attachments on tasks

That is ten major prompts, plus perhaps twenty to thirty iteration prompts for fixes and polish. In total, you might spend four to six hours building FlowTask through vibe coding.

For comparison, a traditional developer building the same application from scratch --- designing the database, writing every component, implementing auth, handling edge cases --- would likely spend forty to sixty hours minimum. Vibe coding compresses that timeline by an order of magnitude.

The key insight: each prompt builds on the context established by previous prompts. The CLAUDE.md file keeps everything coherent. And because you commit after each feature, you can always roll back if something goes wrong.

What's Next

FlowTask is a functional SaaS application, but it is still a closed system. In the next lesson, we are going to open it up. We will build a public API so other applications can interact with FlowTask, add webhook support for real-time integrations, and connect to external services like payment processors and email providers. Your app is about to become part of a larger ecosystem.