Final Project: Auth & Setup
Building a complete SaaS project is like constructing a building—first, you use scaffolding to erect the structural framework; next, you design the electrical and plumbing systems (the database); and finally, you install the door locks (the authentication system).
1. What You'll Learn
- Initialize an enterprise-level project using
create-next-app+ Tailwind + shadcn/ui - Prisma Schema: Five Design Models (User / Organization / Project / Task / Comment) and Enumeration Types
- Auth.js v5 Integration: Dual Authentication Solution Using Credentials and Google OAuth
- Organization Invitation Code System and Member Management
- Middleware Routing Protection and Centralized Session Management
- Database Seeding: Generate 100 sample records
2. The True Story of a Full-Stack Engineer
(1) Pain Point: The Chaotic Start of Building a SaaS Platform from Scratch
Alice is a full-stack engineer at a 50-person startup. The company decided to develop an internal project collaboration platform called “TaskFlow” to serve 10,000 users, each managing 200+ tasks. Alice tried setting up the project structure manually—but the routing became a mess, database migrations frequently failed, and all three authentication solutions she proposed were rejected. The team was wasting four hours a day on environment configuration, and the project was two weeks behind schedule.
(2) Solution Using Scaffolding + Certification Framework
One-click initialization with
create-next-app+ declarative modeling with Prisma ORM + Auth.js out of the box.
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
npx prisma init --datasource-provider postgresql
npx auth add
Three lines of code handle 80% of the initialization work.
(3) Revenue
| Dimension | Manual Setup | TaskFlow Solution |
|---|---|---|
| Initialization Time | 2 days | 2 hours |
| Authentication Solution Development | 1 week (in-house JWT) | 30 minutes (Auth.js integration) |
| Database Migration | Manual SQL Scripts | Prisma Migrate (Declarative) |
| Project Structure Guidelines | Each team has its own style | App Router: Conventional Routing |
| Maintainability | Low (no type safety) | High (TypeScript + Prisma types) |
3. Project Initialization and Configuration
(1) Choosing a create-next-app scaffolding tool
graph LR
A[create-next-app] --> B[TypeScript]
A --> C[Tailwind CSS]
A --> D[ESLint]
A --> E[App Router]
A --> F[src/ Table of Contents]
A --> G[@/ Alias]
style A fill:#cce5ff
style B fill:#d4edda
style C fill:#d4edda
| Option | Value | Description |
|---|---|---|
| TypeScript | Yes | Type safety—a must for production |
| ESLint | Yes | Coding Standards |
| Tailwind CSS | Yes | Works with shadcn/ui |
src/ Table of Contents |
Yes | Separate from app/ for a clear structure |
| App Router | Yes | Next.js 16 default |
| import alias | @/* |
concise import path |
▶ Example: Initializing a TaskFlow Project
Output:
Diagram of the data flow and component structure.
# 1. Create a Project
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
# 2. Go to the Table of Contents
cd taskflow
# 3. Installation shadcn/ui
npx shadcn@latest init -d
# 4. Add shadcn/ui Common Components
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator
# 5. Start the development server
npm run dev
✔ Project created at: taskflow/
✔ shadcn/ui initialized successfully
✔ Component button installed
✔ Component card installed
✔ Component input installed
✔ 7 components installed in total
Output:
✔ Project created at: taskflow/
✔ shadcn/ui initialized successfully
✔ Component button installed
✔ Component card installed
✔ Component input installed
✔ 7 components installed in total
(2) Project Directory Structure
taskflow/
├── src/
│ ├── app/ # App Router Page
│ │ ├── (auth)/ # Authentication-Related Routing Groups
│ │ ├── (dashboard)/ # Dashboard Routing Group
│ │ ├── api/ # API Routes
│ │ ├── layout.tsx # Root Layout
│ │ └── page.tsx # Home
│ ├── components/ # Shared Components
│ │ ├── ui/ # shadcn/ui Components
│ │ └── forms/ # Form Component
│ ├── lib/ # Utility Functions
│ │ ├── auth.ts # Auth.js Layout
│ │ ├── db.ts # Prisma Client
│ │ └── utils.ts # General-Purpose Tools
│ ├── prisma/ # Prisma Schema + Migration
│ │ ├── schema.prisma
│ │ └── seed.ts
│ └── middleware.ts # Route Protection Middleware
├── public/ # Static Resources
├── next.config.ts # Next.js Layout
├── tailwind.config.ts # Tailwind Layout
└── package.json
4. Prisma Schema's Five-Model Design
(1) Data Model Relationship Diagram
graph TB
U[User] -->|belongs to| O[Organization]
O -->|has many| P[Project]
P -->|has many| T[Task]
T -->|has many| C[Comment]
U -->|assigned to| T
U -->|created by| C
U -->|created by| P
style U fill:#cce5ff
style O fill:#d4edda
style P fill:#ffeeba
style T fill:#f8d7da
style C fill:#d6d8db
(2) Defining Enumeration Types
| Enum Name | Value | Description |
|---|---|---|
Role |
OWNER / ADMIN / MEMBER / VIEWER |
Organizational Roles |
ProjectStatus |
ACTIVE / ARCHIVED / COMPLETED |
Project Status |
TaskStatus |
TODO / IN_PROGRESS / DONE |
Task Status |
TaskPriority |
LOW / MEDIUM / HIGH / URGENT |
Task Priority |
(3) Complete Prisma Diagram
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
OWNER
ADMIN
MEMBER
VIEWER
}
enum ProjectStatus {
ACTIVE
ARCHIVED
COMPLETED
}
enum TaskStatus {
TODO
IN_PROGRESS
DONE
}
enum TaskPriority {
LOW
MEDIUM
HIGH
URGENT
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
passwordHash String?
organizationId String?
organization Organization? @relation(fields: [organizationId], references: [id])
role Role @default(MEMBER)
projects Project[]
assignedTasks Task[] @relation("TaskAssignee")
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
inviteCode String @unique @default(cuid())
logo String?
users User[]
projects Project[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Project {
id String @id @default(cuid())
name String
description String?
status ProjectStatus @default(ACTIVE)
organizationId String
organization Organization @relation(fields: [organizationId], references: [id])
creatorId String
creator User @relation(fields: [creatorId], references: [id])
tasks Task[]
color String @default("#3b82f6")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Task {
id String @id @default(cuid())
title String
description String?
status TaskStatus @default(TODO)
priority TaskPriority @default(MEDIUM)
projectId String
project Project @relation(fields: [projectId], references: [id])
assigneeId String?
assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id])
creatorId String
creator User @relation(fields: [creatorId], references: [id])
order Int @default(0)
dueDate DateTime?
attachments Attachment[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Comment {
id String @id @default(cuid())
content String
taskId String
task Task @relation(fields: [taskId], references: [id])
authorId String
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Attachment {
id String @id @default(cuid())
fileName String
fileUrl String
fileSize Int
mimeType String
taskId String
task Task @relation(fields: [taskId], references: [id])
uploaderId String
uploader User @relation(fields: [uploaderId], references: [id])
createdAt DateTime @default(now())
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
▶ Example: Running a database migration
# 1. Create a Migration
npx prisma migrate dev --name init
# 2. Generate Prisma Client
npx prisma generate
# 3. View Database
npx prisma studio
✔ Generated Prisma Client (v6.x) to .\node_modules\@prisma\client
✔ Your database is now in sync with your Prisma schema
✔ Prisma Studio opened at http://localhost:5555
5. Auth.js Authentication Integration
(1) Comparison of Authentication Schemes
| Solution | Advantages | Disadvantages | Applicable Scenarios |
|---|---|---|---|
| Auth.js v5 | Multiple providers, built-in sessions, type-safe | Moderate learning curve | Projects requiring flexible authentication |
| Clerk | Out-of-the-box UI, no configuration required | Paid, vendor-locked | Rapid prototyping |
| Supabase Auth | Generous free quota, integrated with the database | Requires integration with the Supabase ecosystem | Fully managed solution |
| Custom-developed JWT | Full control | High security risks, high development costs | Not recommended |
(2) Auth.js Configuration
// src/lib/auth.ts
import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import Google from "next-auth/providers/google"
import { compare } from "bcryptjs"
import { prisma } from "./db"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null
}
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
})
if (!user || !user.passwordHash) {
return null
}
const isValid = await compare(
credentials.password as string,
user.passwordHash
)
if (!isValid) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
}
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string
}
return session
},
},
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
session: {
strategy: "jwt",
},
})
▶ Example: Auth.js API Routes
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth"
export const { GET, POST } = handlers
Output:
TypeScript code executed successfully.
// src/app/api/auth/signup/route.ts
import { NextResponse } from "next/server"
import { hash } from "bcryptjs"
import { prisma } from "@/lib/db"
export async function POST(request: Request) {
try {
const { name, email, password, inviteCode } = await request.json()
const existingUser = await prisma.user.findUnique({
where: { email },
})
if (existingUser) {
return NextResponse.json(
{ error: "Email already registered" },
{ status: 400 }
)
}
const passwordHash = await hash(password, 12)
let organizationId: string | null = null
let role: "OWNER" | "ADMIN" | "MEMBER" | "VIEWER" = "MEMBER"
if (inviteCode) {
const org = await prisma.organization.findUnique({
where: { inviteCode },
})
if (org) {
organizationId = org.id
}
}
const user = await prisma.user.create({
data: {
name,
email,
passwordHash,
organizationId,
role: organizationId ? "MEMBER" : "OWNER",
},
})
if (!organizationId) {
const org = await prisma.organization.create({
data: {
name: `${name}'s Organization`,
slug: email.split("@")[0],
users: { connect: { id: user.id } },
},
})
await prisma.user.update({
where: { id: user.id },
data: { organizationId: org.id, role: "OWNER" },
})
}
return NextResponse.json(
{ message: "User created successfully" },
{ status: 201 }
)
} catch (error) {
return NextResponse.json(
{ error: "Something went wrong" },
{ status: 500 }
)
}
}
(3) Organization Invitation Code System
// src/lib/invite.ts
import { prisma } from "./db"
export async function generateInviteCode(organizationId: string) {
const org = await prisma.organization.update({
where: { id: organizationId },
data: { inviteCode: crypto.randomUUID().slice(0, 8) },
})
return org.inviteCode
}
export async function validateInviteCode(code: string) {
// TKFLW-XXXXXX format
const fullCode = `TKFLW-${code.toUpperCase()}`
const org = await prisma.organization.findUnique({
where: { inviteCode: fullCode },
})
return org
}
▶ Example: Login and Registration Pages
Output:
Updates the database record.
// src/app/(auth)/auth/signin/page.tsx
import { AuthCard } from "@/components/auth/auth-card"
import { SignInForm } from "@/components/auth/sign-in-form"
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<AuthCard
title="Sign in to TaskFlow"
description="Enter your credentials to continue"
>
<SignInForm />
</AuthCard>
</div>
)
}
Output:
Renders the SignInPage component UI.
// src/components/auth/sign-in-form.tsx
"use client"
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Card, CardContent } from "@/components/ui/card"
export function SignInForm() {
const router = useRouter()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
setIsLoading(true)
setError(null)
const formData = new FormData(event.currentTarget)
const result = await signIn("credentials", {
email: formData.get("email") as string,
password: formData.get("password") as string,
redirect: false,
})
if (result?.error) {
setError("Invalid email or password")
setIsLoading(false)
return
}
router.push("/dashboard")
router.refresh()
}
return (
<Card>
<CardContent className="pt-6">
<form onSubmit={onSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="alice@example.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
name="password"
type="password"
placeholder="Enter your password"
required
/>
</div>
{error && (
<p className="text-sm text-red-500">{error}</p>
)}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? "Signing in..." : "Sign in"}
</Button>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<Button
type="button"
variant="outline"
className="w-full"
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
Sign in with Google
</Button>
</form>
</CardContent>
</Card>
)
}
6. Middleware Route Protection
(1) Middleware Workflow
graph LR
A[Request] --> B{Middleware}
B -->|Public path| C[Allow]
B -->|Protected path| D{Has session?}
D -->|Yes| C
D -->|No| E[Redirect /auth/signin]
B -->|API route| F{Valid token?}
F -->|Yes| G[Forward]
F -->|No| H[401 Unauthorized]
style B fill:#ffeeba
style C fill:#d4edda
style E fill:#f8d7da
(2) Middleware Implementation
// src/middleware.ts
import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"
export default auth((req) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
const isApiRoute = nextUrl.pathname.startsWith("/api")
const isAuthRoute = nextUrl.pathname.startsWith("/auth")
const isDashboardRoute = nextUrl.pathname.startsWith("/dashboard")
const isPublicRoute =
nextUrl.pathname === "/" ||
nextUrl.pathname.startsWith("/_next") ||
nextUrl.pathname.startsWith("/static") ||
nextUrl.pathname === "/api/auth/signup"
if (isApiRoute && !isPublicRoute && !isLoggedIn) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
if (isDashboardRoute && !isLoggedIn) {
return NextResponse.redirect(new URL("/auth/signin", nextUrl))
}
if (isAuthRoute && isLoggedIn) {
return NextResponse.redirect(new URL("/dashboard", nextUrl))
}
return NextResponse.next()
})
export const config = {
matcher: [
"/((?!_next/static|_next/image|favicon.ico).*)",
],
}
▶ Example: Testing Middleware Route Protection
# Accessing Protected Routes(Not logged in)
curl -v http://localhost:3000/dashboard 2>&1 | grep Location
# Visit API Routing(Not logged in)
curl -v http://localhost:3000/api/projects 2>&1
< Location: http://localhost:3000/auth/signin
< HTTP/1.1 302 Found
{"error":"Unauthorized"}
(3) Matcher Configuration Strategies
| Pattern | Matching Paths | Excluded Paths |
|---|---|---|
/((?!_next/static).*) |
All Routes | Static Resources |
/((?!api/auth).*) |
All Routes | Authentication API |
| `/((?!_next/static | _next/image).*)` | All Routes |
7. Database Seeding
(1) Seeding Strategy
graph TB
A[seed.ts] --> B[Create Organization]
B --> C[Create Users x5]
C --> D[Create Projects x5]
D --> E[Create Tasks x50]
E --> F[Create Comments x40]
F --> G[Create Attachments x10]
style A fill:#cce5ff
style G fill:#d4edda
▶ Example: Complete Seeding Script
Output:
Diagram of seeding flow: create organization → users → projects → tasks → comments.
// src/prisma/seed.ts
import { PrismaClient, Role, TaskStatus, TaskPriority } from "@prisma/client"
import { hash } from "bcryptjs"
const prisma = new PrismaClient()
async function main() {
console.log("🌱 Seeding database...")
const passwordHash = await hash("password123", 12)
const org = await prisma.organization.create({
data: {
name: "Acme Corp",
slug: "acme-corp",
inviteCode: "TKFLW-ACMECORP",
},
})
const users = await Promise.all([
prisma.user.create({
data: {
name: "Alice Wang",
email: "alice@acme.com",
passwordHash,
organizationId: org.id,
role: "OWNER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=alice",
},
}),
prisma.user.create({
data: {
name: "Bob Chen",
email: "bob@acme.com",
passwordHash,
organizationId: org.id,
role: "ADMIN",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=bob",
},
}),
prisma.user.create({
data: {
name: "Charlie Liu",
email: "charlie@acme.com",
passwordHash,
organizationId: org.id,
role: "MEMBER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=charlie",
},
}),
prisma.user.create({
data: {
name: "Diana Park",
email: "diana@acme.com",
passwordHash,
organizationId: org.id,
role: "MEMBER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=diana",
},
}),
prisma.user.create({
data: {
name: "Eve Zhang",
email: "eve@acme.com",
passwordHash,
organizationId: org.id,
role: "VIEWER",
image: "https://api.dicebear.com/7.x/avataaars/svg?seed=eve",
},
}),
])
const projectData = [
{ name: "Website Redesign", description: "Redesign the company website with modern UI", color: "#3b82f6" },
{ name: "Mobile App v2", description: "Version 2 of the mobile application", color: "#10b981" },
{ name: "API Integration", description: "Third-party API integrations", color: "#f59e0b" },
{ name: "Data Migration", description: "Migrate data from legacy system", color: "#ef4444" },
{ name: "Security Audit", description: "Q3 security audit and compliance", color: "#8b5cf6" },
]
const projects = await Promise.all(
projectData.map((p) =>
prisma.project.create({
data: {
...p,
organizationId: org.id,
creatorId: users[0].id,
},
})
)
)
const taskTitles = [
"Set up CI/CD pipeline", "Design system components", "Write unit tests",
"Implement user authentication", "Create database schema", "API documentation",
"Performance optimization", "Accessibility audit", "Mobile responsive layout",
"Error handling middleware", "Data validation", "Search functionality",
"Notification system", "File upload feature", "User dashboard",
"Admin panel", "Reporting module", "Email templates",
"WebSocket integration", "Cache layer", "Rate limiting",
"Logging system", "Backup strategy", "Monitoring setup",
"Load testing",
]
const statuses: TaskStatus[] = ["TODO", "IN_PROGRESS", "DONE"]
const priorities: TaskPriority[] = ["LOW", "MEDIUM", "HIGH", "URGENT"]
for (const project of projects.slice(0, 3)) {
const taskCount = project.name === "Website Redesign" ? 20 : 15
for (let i = 0; i < taskCount; i++) {
const task = await prisma.task.create({
data: {
title: taskTitles[i % taskTitles.length],
description: `Detailed description for task #${i + 1} in ${project.name}`,
status: statuses[i % statuses.length],
priority: priorities[i % priorities.length],
projectId: project.id,
assigneeId: users[i % users.length].id,
creatorId: users[0].id,
order: i,
dueDate: new Date(Date.now() + (i + 1) * 86400000 * 7),
},
})
if (i % 3 === 0) {
await prisma.comment.create({
data: {
content: `This task is progressing well. Need to review the implementation.`,
taskId: task.id,
authorId: users[(i + 1) % users.length].id,
},
})
}
}
}
const totalTasks = await prisma.task.count()
const totalComments = await prisma.comment.count()
console.log(`✅ Seeding complete:
- 1 Organization (${org.name})
- ${users.length} Users
- ${projects.length} Projects
- ${totalTasks} Tasks
- ${totalComments} Comments`)
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
Output:
🌱 Seeding database...
`✅ Seeding complete:
- 1 Organization (${org.name}
e
# package.json Add seed Screenplay
# "prisma": { "seed": "tsx src/prisma/seed.ts" }
# Run seeding
npx prisma db seed
🌱 Seeding database...
✅ Seeding complete:
- 1 Organization (Acme Corp)
- 5 Users
- 5 Projects
- 50 Tasks
- 16 Comments
8. Complete Example: The Entire Process from Project Initialization to Authentication
# ============================================
# TaskFlow Complete Initialization Process
# ============================================
# 1. Create a Project
npx create-next-app@latest taskflow --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd taskflow
# 2. Install Dependencies
npm install next-auth@beta @prisma/client @auth/prisma-adapter bcryptjs
npm install prisma tsx --save-dev
npm install @types/bcryptjs --save-dev
# 3. Initialization shadcn/ui
npx shadcn@latest init -d
npx shadcn@latest add button card input label select dialog dropdown-menu avatar badge separator
# 4. Initialization Prisma
npx prisma init --datasource-provider postgresql
# Copy schema.prisma Content
npx prisma migrate dev --name init
npx prisma generate
# 5. Set Environment Variables (.env)
cat > .env << EOF
DATABASE_URL="postgresql://postgres:password@localhost:5432/taskflow"
AUTH_SECRET="my-super-secret-key-change-in-production"
AUTH_URL="http://localhost:3000"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
EOF
# 6. Run Seeding
npx prisma db seed
# 7. Start the development server
npm run dev
Expected output (when accessing http://localhost:3000 in a browser):
→ Automatically redirect to /auth/signin(Not logged in)
→ Display the login form(Email + Password + Google)
→ Usage alice@acme.com / password123 Log In
→ Jump to /dashboard
→ Sidebar Display:Acme Corp · 5 Projects · 50 Tasks
❓ FAQ
npx prisma migrate dev --name <description> to create a new migration; Prisma will automatically generate and execute the SQL. In a development environment, you can use npx prisma db push to sync directly (without generating migration files).TKFLW-ACMECORP. The combination of the prefix and the organization slug makes the invitation code highly readable and unique./((?!_next/static|_next/image|favicon.ico).*) matches all non-static resource paths, allowing the middleware to perform authentication checks on all page routes and API routes. Next.js recommends performing path checks within the middleware rather than over-segmenting them in the matcher.npm i tsx -D), which can run .ts files directly without compilation. Prisma officially recommends tsx as the seed runner. You can also use ts-node, but tsx is faster and offers better compatibility.passwordHash optional in the User model?Credentials, passwordHash has a value; for OAuth users, passwordHash is null. This demonstrates the flexibility of the schema design.📖 Summary
create-next-appChoosing a Framework: TypeScript + Tailwind + App Router + src Directory as a Starting Point for Enterprise-Level Projects- shadcn/ui provides a dependency-free UI component library that can be installed on demand without increasing the package size
- Prisma Schema uses declarative syntax to define five models—User, Organization, Project, Task, and Comment—as well as enumeration types.
- Auth.js v5 supports dual authentication via Credentials and Google OAuth; the JWT Session policy does not require database storage for sessions.
- Middleware implements global route protection through the
auth()helper and matcher configuration - The organization invitation code system (prefixed with
TKFLW-) allows team members to join on their own - Database seeding uses the tsx driver, which can generate over 100 real demo records
📝 Exercises
-
Basic Exercise (⭐): Follow the steps in this lesson to initialize the TaskFlow project, successfully run
npm run dev, and view the login page in your browser. Take a screenshot of the results and save it. -
Advanced Exercise (⭐⭐): Add a
Labelmodel (tag system) to Prisma Schema, establish a many-to-many relationship withTask(via the intermediate tableTaskLabel), then run the migration and generate the types. Implement a CRUD API route for tags. -
Challenge (⭐⭐⭐): Extend the Auth.js configuration to add a GitHub OAuth provider; implement the role-checking logic described in
middleware.ts—only users with theOWNERandADMINroles can access the/dashboard/adminpath; write unit tests to verify the role guard logic.



