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(),
});
Hooked up via react-hook-form:
const { register, handleSubmit, formState: { errors } } = useForm<LoginSchema>({
resolver: zodResolver(loginSchema),
});
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,
});
}
The frontend ensures the CSRF token is loaded at runtime:
useEffect(() => {
if (!document.cookie.includes('csrf_token')) {
fetch('/api/auth/csrf', { credentials: 'include' });
}
}, []);
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)
);
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();
});
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 }}
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)