DEV Community

Cover image for How I Built a Secure, Scalable Auth System in Next.js 15 (with JWT, Edge Middleware, and Drizzle)
Nidal tahir
Nidal tahir

Posted on

How I Built a Secure, Scalable Auth System in Next.js 15 (with JWT, Edge Middleware, and Drizzle)

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
Enter fullscreen mode Exit fullscreen mode

Flow breakdown:

  1. User authentication → Credentials verified against database
  2. JWT generation → Secure token created with 24h expiration
  3. HTTP-only cookie → Token stored safely, immune to XSS
  4. Edge middleware → Lightning-fast route protection
  5. 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
  });
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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`)
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Registration flow: Create new accounts
  2. Login validation: Test credential verification
  3. Route protection: Verify middleware functionality
  4. Session management: Test token expiration
  5. 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)