DEV Community

Cover image for How I Built an Enterprise Login Flow with CSRF, JWT, Zod & Playwright in Next.js 15
Cedric Jaden Heijlman
Cedric Jaden Heijlman

Posted on

How I Built an Enterprise Login Flow with CSRF, JWT, Zod & Playwright in Next.js 15

Why I Built This

For my project — an enterprise-grade invoicing platform for freelancers — I didn't want to ship a generic login screen. I wanted something real:

✅ Client-side validation with clean UX
✅ CSRF protection for secure API calls
✅ JWT-based authentication via Edge Middleware
✅ End-to-end testing with Playwright
✅ CI/CD pipeline with automatic deployment via GitHub Actions and Vercel

This is not a demo tutorial.
This is production-level code running in a live system.

🧱 Tech Stack

Layer Tool
Framework Next.js 15 (App Router)
Styling Tailwind CSS
Forms React Hook Form + Zod
Auth Supabase JWT
Security CSRF via Edge Middleware
Testing Playwright (E2E)
CI/CD GitHub Actions + Vercel

Step 1: Form Validation with Zod + React Hook Form

// lib/schemas/loginSchema.ts
export const loginSchema = z.object({
  email: z
    .string({ required_error: 'Email is required' })
    .email({ message: 'Enter a valid email address' }),
  password: z
    .string({ required_error: 'Password is required' })
    .min(8, { message: 'At least 8 characters required' })
    .regex(/[A-Z]/, 'At least one uppercase letter required')
    .regex(/[0-9]/, 'At least one number required'),
  rememberMe: z.boolean().optional(),
});
Enter fullscreen mode Exit fullscreen mode

Hooked up via react-hook-form:

const { register, handleSubmit, formState: { errors } } = useForm<LoginSchema>({
  resolver: zodResolver(loginSchema),
});
Enter fullscreen mode Exit fullscreen mode

Step 2: CSRF Protection at the Edge

A custom csrf_token cookie is set via Edge Middleware:

if (!csrfCookie) {
  const newToken = crypto.randomUUID();
  res.cookies.set('csrf_token', newToken, {
    httpOnly: false,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60,
  });
}
Enter fullscreen mode Exit fullscreen mode

The frontend ensures the CSRF token is loaded at runtime:

useEffect(() => {
  if (!document.cookie.includes('csrf_token')) {
    fetch('/api/auth/csrf', { credentials: 'include' });
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

Step 3: JWT Authentication via Supabase

After successful login:

  • The server stores a Supabase JWT in a cookie (access_token)
  • Middleware validates it with jose:
const { payload } = await jwtVerify(
  token,
  new TextEncoder().encode(SUPABASE_JWT_SECRET)
);
Enter fullscreen mode Exit fullscreen mode

Also:

  • ✅ Redirects authenticated users to /dashboard:
  • ✅ Redirects unauthenticated users back to /login:

Step 4: End-to-End Testing with Playwright

test('shows validation errors on empty submit', async ({ page }) => {
  await page.goto('/login');
  await page.getByRole('button', { name: 'Log In' }).click();

  await expect(page.getByText('Enter your email')).toBeVisible();
  await expect(page.getByText('Enter your password')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

Other scenarios tested:

  • ❌ Invalid input (email format, short password)
  • ❌ Wrong credentials (shows toast)
  • ✅ Successful login → dashboard
  • 🛡️ Missing CSRF → 403 error

Step 5: CI/CD with GitHub Actions + Vercel

- name: Deploy to Vercel
  uses: amondnet/vercel-action@v25
  with:
    vercel-token: ${{ secrets.VERCEL_TOKEN }}
    vercel-org-id: ${{ secrets.VERCEL_TEAM_ID }}
    vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
Enter fullscreen mode Exit fullscreen mode

The full pipeline includes:

  • npm run lint
  • npm run test (unit + e2e)
  • npm run build
  • Auto-deploy to Vercel (with preview domains for staging)

Additional UX Highlights

  • Tailwind-powered layout + animations
  • Password visibility toggle
  • Accessible custom checkbox
  • Inline error messages
  • Toast feedback for backend errors
  • Fully responsive and accessible

Feedback? Let’s Connect!

Have feedback, questions, or want to collaborate?
Drop a comment or message — always happy to connect with devs building real-world systems 🚀

About Me

Cedric Heijlman
19-year-old fullstack developer from the Netherlands 🇳🇱
Building futuristic SaaS tools and AI integrations using Next.js, Supabase, and modern open-source tech.

Follow for more deep-dives into security, architecture and developer branding 🚀

Top comments (0)