404 Not Found

404 Not Found


nginx

Comprehensive Project Initialization and Authentication: Building the TaskFlow SaaS Platform from the Ground Up

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


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.

BASH
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

100%
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

BASH
# 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
TEXT
✔ 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

TEXT
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

100%
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

PRISMA
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

BASH
# 1. Create a Migration
npx prisma migrate dev --name init

# 2. Generate Prisma Client
npx prisma generate

# 3. View Database
npx prisma studio
TEXT
✔ 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

TYPESCRIPT
// 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

TYPESCRIPT
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth"

export const { GET, POST } = handlers
TYPESCRIPT
// 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

TYPESCRIPT
// 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

TSX
// 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>
  )
}
TSX
// 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

100%
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

TYPESCRIPT
// 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

BASH
# 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
TEXT
< 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

100%
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

TYPESCRIPT
// 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()
  })
BASH
# package.json Add seed Screenplay
# "prisma": { "seed": "tsx src/prisma/seed.ts" }

# Run seeding
npx prisma db seed
TEXT
🌱 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

BASH
# ============================================
# 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):

TEXT
→ 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

Q Why use Auth.js instead of Clerk?
A Auth.js (NextAuth v5) is an open-source solution that supports over 80 providers and avoids vendor lock-in. Clerk is suitable for rapid prototyping but becomes quite expensive once you start paying for it. With Auth.js, TaskFlow can fully control the authentication logic and integrates seamlessly with Prisma.
Q How do I sync the database after modifying the Prisma schema?
A Run 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).
Q What is the purpose of the prefix in the invitation code TKFLW-?
A The prefix identifies the code as a TaskFlow invitation code to avoid confusion with other systems. Example of a complete invitation code: TKFLW-ACMECORP. The combination of the prefix and the organization slug makes the invitation code highly readable and unique.
Q Why does the matcher configuration in middleware match all paths?
A The matcher configuration /((?!_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.
Q What is tsx in Seeding?
A tsx is the TypeScript executor (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.
Q Why is passwordHash optional in the User model?
A Because Google OAuth login is supported, and OAuth users do not require a password. For users logging in with Credentials, passwordHash has a value; for OAuth users, passwordHash is null. This demonstrates the flexibility of the schema design.
Q How are sessions shared across multiple providers?
A Auth.js’s JWT policy handles this automatically. Whether users log in using credentials or Google, the generated JWT token contains the same userId. In the session callback, the database is queried using the userId to retrieve the user’s full information.

📖 Summary

📝 Exercises

  1. 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.

  2. Advanced Exercise (⭐⭐): Add a Label model (tag system) to Prisma Schema, establish a many-to-many relationship with Task (via the intermediate table TaskLabel), then run the migration and generate the types. Implement a CRUD API route for tags.

  3. Challenge (⭐⭐⭐): Extend the Auth.js configuration to add a GitHub OAuth provider; implement the role-checking logic described in middleware.ts—only users with the OWNER and ADMIN roles can access the /dashboard/admin path; write unit tests to verify the role guard logic.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏