Auth with Server Actions

Next.js has no built-in auth system — you wire up sessions yourself (or use a library like NextAuth/Auth.js, Clerk, etc.). This demo shows the raw mechanism: a Server Action sets an httpOnly cookie, and a proxy (middleware) checks that cookie before allowing access to /dashboard.

Explain it like I'm 5

A Server Actionis a function that lives on the server but you can call it directly from a form's action prop — no manual fetch, no API route needed for this part. Submitting the form below runs real server code that sets a cookie identifying you, then redirects you to the protected dashboard.

1. The Server Action

'use server' at the top of the file marks every exported function as a Server Action. It receives the submitted FormData directly, can set cookies, and can redirect() — all without you writing a fetch call or an API route.
app/actions/auth.ts
'use server'
import { redirect } from 'next/navigation'
import { createSession } from '@/lib/session'

export async function login(formData: FormData) {
  const username = formData.get('username')
  if (typeof username !== 'string' || !username.trim()) {
    redirect('/login?error=missing-username')
  }
  await createSession(username.trim())   // sets an httpOnly cookie
  redirect('/dashboard')
}

2. The form that calls it

Passing the Server Action straight to <form action={login}> is the whole integration — React/Next handles the network round-trip, no onSubmit or fetch required.
app/login/page.tsx
<form action={login}>
  <input name="username" placeholder="Any username" />
  <button type="submit">Log in</button>
</form>

Live result

3. The proxy (middleware) guarding /dashboard

Renamed from middleware.ts to proxy.ts in Next.js 16. It runs before the route renders, on every matching request, and can redirect unauthenticated visitors before any page code executes.
proxy.ts
import { NextResponse, type NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  const session = request.cookies.get('nextnotes_session')
  if (!session) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  return NextResponse.next()
}

export const config = { matcher: '/dashboard/:path*' }