Authentication is the backbone of most modern web applications, yet it's often one of the most challenging aspects to implement securely. With Next.js 15's new features and improved App Router, building a robust authentication system has become more streamlined than ever.
In this guide, I'll walk you through creating a production-ready authentication system that balances security, performance, and developer experience. We'll cover everything from JWT implementation to edge middleware optimization.
👉 Full source code available on GitHub: auth-next-my-app
Why This Architecture?
Before diving into code, let's understand the key decisions behind this implementation:
JWT over Server Sessions: While server sessions are excellent for traditional applications, JWTs offer stateless authentication that scales horizontally without session stores. Perfect for modern serverless deployments.
HTTP-only Cookies: Unlike localStorage, HTTP-only cookies are immune to XSS attacks. They're automatically included in requests and can't be accessed by malicious scripts.
Edge Middleware: Next.js 15's middleware runs at the edge, meaning authentication checks happen before your application code even loads—resulting in faster redirects and better UX.
Authentication Flow Overview
Here's how our secure authentication system works:
User Request → Edge Middleware → JWT Verification → Protected Route
↓ ↓ ↓ ↓
Sign In/Up → HTTP-Only Cookie → Token Validation → Dashboard Access
↓ ↓ ↓ ↓
Database ← Password Hash ← bcrypt Verification ← Authorized User
Flow breakdown:
- User authentication → Credentials verified against database
- JWT generation → Secure token created with 24h expiration
- HTTP-only cookie → Token stored safely, immune to XSS
- Edge middleware → Lightning-fast route protection
- Protected access → Seamless user experience
The Tech Stack
Our authentication system leverages:
- Next.js 15.1.8 with App Router
- TypeScript for type safety
-
JWT (jose) for token management - If you're not familiar with
jose
, it's a zero-dependency library for working with JWTs—lightweight and perfect for edge runtimes - bcryptjs for password hashing
- Drizzle ORM with SQLite - Drizzle is a type-safe ORM that generates SQL queries at build time, offering better performance than traditional ORMs
- Zod for validation - Zod provides runtime type checking and validation, ensuring your API receives exactly the data it expects
- Tailwind CSS + Radix UI for components
Core Authentication Flow
1. User Registration
The registration process implements several security layers:
// Simplified registration flow
async function registerUser(data: RegisterData) {
// 1. Validate input with Zod
const validated = registerSchema.parse(data);
// 2. Hash password with bcrypt (10 rounds)
const hashedPassword = await bcrypt.hash(validated.password, 10);
// 3. Save user to database
const user = await db.insert(users).values({
email: validated.email,
password: hashedPassword,
name: validated.name
});
// 4. Generate JWT token
const token = await new SignJWT({ userId: user.id })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('24h')
.sign(secret);
// 5. Set HTTP-only cookie
cookies().set('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 86400 // 24 hours
});
}
Security highlights:
- 10 rounds of bcrypt: Provides strong protection against rainbow table attacks
- 24-hour token expiration: Limits exposure window if tokens are compromised
- Strict SameSite policy: Prevents CSRF attacks
2. Route Protection with Middleware
Next.js 15's middleware runs at the edge, making it incredibly fast:
// middleware.ts
export async function middleware(request: NextRequest) {
// Protected routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
const token = request.cookies.get('token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
try {
// Verify JWT at the edge
await jwtVerify(token, secret);
} catch {
return NextResponse.redirect(new URL('/sign-in', request.url));
}
}
return NextResponse.next();
}
Performance benefits:
- Edge computing: Authentication checks happen closest to users
- No server round-trips: Redirects happen before hitting your application
- Reduced latency: Faster than traditional server-side checks
3. API Route Implementation
The API routes use Next.js 15's improved App Router structure:
// app/api/auth/login/route.ts
export async function POST(request: Request) {
try {
const { email, password } = await request.json();
// Find user in database
const user = await db.select().from(users)
.where(eq(users.email, email))
.limit(1);
if (!user.length) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Verify password
const validPassword = await bcrypt.compare(password, user[0].password);
if (!validPassword) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Generate and set token...
} catch (error) {
return NextResponse.json(
{ error: 'Something went wrong' },
{ status: 500 }
);
}
}
Database Design & ORM Choice
Why Drizzle ORM?
Drizzle offers several advantages over alternatives:
- Type-safe queries: Full TypeScript support with auto-completion
- Zero runtime overhead: Queries are as fast as raw SQL
- Edge-compatible: Works perfectly with serverless functions
- Simple migrations: Easy schema evolution
// Schema definition
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
password: text('password').notNull(),
name: text('name').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.default(sql`CURRENT_TIMESTAMP`)
});
SQLite vs PostgreSQL
Development: SQLite provides zero-config setup—perfect for getting started quickly.
Production: The system seamlessly transitions to PostgreSQL on platforms like Vercel, where serverless functions require managed databases.
Security Best Practices Implemented
1. Password Security
- bcrypt with 10 rounds: Industry standard for password hashing
- No password storage: Original passwords never touch the database
2. Token Security
- Short expiration: 24-hour tokens limit breach impact
- HTTP-only cookies: Prevents XSS token theft
- Secure flags: HTTPS-only in production
3. Input Validation
- Zod schemas: Runtime type checking prevents injection attacks
- Server-side validation: Never trust client data
4. Error Handling
- Generic error messages: Prevents user enumeration attacks
- Proper status codes: Clear API responses without revealing system details
Performance Optimizations
1. Edge Middleware
Authentication checks happen at CDN edge locations, reducing latency by up to 50% compared to server-side checks.
2. Efficient Database Queries
// Optimized user lookup
const user = await db.select({
id: users.id,
email: users.email,
password: users.password
}).from(users)
.where(eq(users.email, email))
.limit(1); // Always limit queries
3. Database Strategy: SQLite vs PostgreSQL
For Development & Small Apps:
- SQLite + Drizzle provides zero-config setup and excellent performance
- Perfect for MVPs, personal projects, and teams getting started quickly
- File-based database means no external dependencies
For Production & Scale:
- PostgreSQL + Drizzle offers better concurrency and advanced features
- Essential for multi-tenant applications or high-traffic scenarios
- Serverless platforms (Vercel, Netlify) require managed databases anyway
Migration tip: Drizzle makes switching between databases seamless—same ORM, different connection string.
4. Component Optimization
UI components use React Server Components where possible, reducing client-side JavaScript bundle size.
Deployment Considerations
Local Development
# Quick setup
git clone https://github.com/nidal1111/auth-next-my-app.git
cd auth-next-my-app
npm install
cp .env.local.example .env.local
# Add your JWT_SECRET
npm run db:push
npm run dev
Production Deployment
The system includes production configurations for:
- Vercel: Automatic PostgreSQL integration
- Traditional servers: PM2 configurations included
- Docker: Multi-stage builds for optimal performance
Testing Your Implementation
The repository includes comprehensive testing scenarios:
- Registration flow: Create new accounts
- Login validation: Test credential verification
- Route protection: Verify middleware functionality
- Session management: Test token expiration
- Security headers: Validate cookie settings
What's Next?
This implementation provides a solid foundation, but consider these enhancements:
- OAuth integration (Google, GitHub)
- Two-factor authentication
- Password reset functionality
- Rate limiting for login attempts
- Session management dashboard
Get the Complete Code
The full implementation is available on GitHub: auth-next-my-app
Use it as:
- 🚀 Starter template for your next project
- 📚 Learning resource for authentication patterns
- 🔧 Reference implementation for security best practices
Contributing
Found a bug or have an improvement idea? The repository welcomes contributions! Whether it's:
- Security enhancements
- Performance optimizations
- Documentation improvements
- New feature implementations
Open a Pull Request and help make this authentication system even better for the community.
Building secure authentication doesn't have to be overwhelming. With the right architecture and tools, you can create a system that's both secure and maintainable. This implementation demonstrates that production-ready authentication is achievable with modern Next.js patterns.
What authentication challenge are you facing in your current project? Share in the comments below!
Top comments (0)