Learn how to implement user authentication in a Next.js application using Nhost
This tutorial part builds upon the Protected Routes part by adding complete email/password authentication with email verification functionality. You’ll implement sign up, sign in, email verification, and sign out features to create a full authentication flow.
This is Part 3 in the Full-Stack Next.js Development with Nhost series. This part creates a production-ready authentication system with secure email verification and proper error handling using Next.js App Router.
In this step, we’ll create a complete sign-in flow using Next.js App Router patterns. We’ll build three key files: a server component for the main page that handles URL parameters, a client component for the interactive form, and server actions for secure authentication processing.
Page Component
Form Component
Server Actions
The main sign-in page is a server component that handles URL parameters (like error messages) and renders the sign-in form. This component runs on the server and can access search parameters directly.
src/app/signin/page.tsx
Copy
Ask AI
import Link from "next/link";import SignInForm from "./SignInForm";export default async function SignIn({ searchParams,}: { searchParams: Promise<{ error?: string }>;}) { // Extract error from URL parameters const params = await searchParams; const error = params?.error; return ( <div> <h1>Sign In</h1> <SignInForm initialError={error} /> <div className="auth-links"> <p> Don't have an account? <Link href="/signup">Sign Up</Link> </p> </div> </div> );}
The sign-in form is a client component that handles user interactions, loading states, and form submissions. It communicates with server actions and provides real-time feedback to users.
Server actions handle the authentication logic securely on the server side. They validate form data, communicate with Nhost, and return appropriate responses for success or error states.
src/app/signin/actions.ts
Copy
Ask AI
"use server";import type { ErrorResponse } from "@nhost/nhost-js/auth";import type { FetchError } from "@nhost/nhost-js/fetch";import { createNhostClient } from "../../lib/nhost/server";export async function signIn(formData: FormData) { const email = formData.get("email") as string; const password = formData.get("password") as string; if (!email || !password) { return { error: "Email and password are required", }; } try { const nhost = await createNhostClient(); const response = await nhost.auth.signInEmailPassword({ email, password, }); if (response.body?.session) { return { redirect: "/profile" }; } else { return { error: "Failed to sign in. Please check your credentials.", }; } } catch (err) { const error = err as FetchError<ErrorResponse>; return { error: `An error occurred during sign in: ${error.message}`, }; }}
In this step, we’ll build the user registration system with email verification support. The sign-up flow includes handling both the registration form and the email verification success state, all using Next.js server and client components.
Page Component
Form Component
Server Actions
The sign-up page is a server component that manages different states: showing the registration form or displaying the email verification success message. It handles URL parameters to determine which state to render.
src/app/signup/page.tsx
Copy
Ask AI
import Link from "next/link";import SignUpForm from "./SignUpForm";export default async function SignUp({ searchParams,}: { searchParams: Promise<{ error?: string; verify?: string; email?: string; }>;}) { // Extract parameters from URL const params = await searchParams; const error = params?.error; const verificationSent = params?.verify === "success"; const email = params?.email; if (verificationSent) { return ( <div> <h1>Check Your Email</h1> <div className="success-message"> <p> We've sent a verification link to <strong>{email}</strong> </p> <p> Please check your email and click the verification link to activate your account. </p> </div> <p> <Link href="/signin">Back to Sign In</Link> </p> </div> ); } return ( <div> <h1>Sign Up</h1> <SignUpForm initialError={error} /> <div className="auth-links"> <p> Already have an account? <Link href="/signin">Sign In</Link> </p> </div> </div> );}
The registration form is a client component that collects user information (display name, email, password) and handles form validation, loading states, and error feedback during the sign-up process.
Server actions handle user registration with Nhost, including setting up email verification. They process form data, create user accounts, and coordinate the email verification flow by setting the appropriate redirect URLs.
src/app/signup/actions.ts
Copy
Ask AI
"use server";import type { ErrorResponse } from "@nhost/nhost-js/auth";import type { FetchError } from "@nhost/nhost-js/fetch";import { createNhostClient } from "../../lib/nhost/server";export async function signUp(formData: FormData) { const email = formData.get("email") as string; const password = formData.get("password") as string; const displayName = formData.get("displayName") as string; if (!email || !password || !displayName) { return { error: "All fields are required", }; } try { const nhost = await createNhostClient(); const response = await nhost.auth.signUpEmailPassword({ email, password, options: { displayName, // Set the redirect URL for email verification redirectTo: `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"}/verify`, }, }); if (response.body?.session) { // Successfully signed up and automatically signed in return { redirect: "/profile" }; } else { // Verification email sent return { redirect: `/signup?verify=success&email=${encodeURIComponent(email)}`, }; } } catch (err) { const error = err as FetchError<ErrorResponse>; return { error: `An error occurred during sign up: ${error.message}`, }; }}
In this step, we’ll implement email verification using Next.js Route Handlers. When users click the verification link in their email, it will process the token server-side and either redirect them to their profile or show an error page with debugging information.
Route Handler
Error Page
The verification Route Handler is a server-side API endpoint that processes email verification tokens. It validates the token, handles edge cases (like already signed-in users), and redirects appropriately based on the verification result.
src/app/verify/route.ts
Copy
Ask AI
import type { ErrorResponse } from "@nhost/nhost-js/auth";import type { FetchError } from "@nhost/nhost-js/fetch";import type { NextRequest } from "next/server";import { NextResponse } from "next/server";import { createNhostClient } from "../../lib/nhost/server";export async function GET(request: NextRequest) { const refreshToken = request.nextUrl.searchParams.get("refreshToken"); if (!refreshToken) { // Collect all query parameters for debugging const params = new URLSearchParams(request.nextUrl.searchParams); params.set("message", "No refresh token provided"); return NextResponse.redirect( new URL(`/verify/error?${params.toString()}`, request.url), ); } try { const nhost = await createNhostClient(); if (nhost.getUserSession()) { // Collect all query parameters const params = new URLSearchParams(request.nextUrl.searchParams); params.set("message", "Already signed in"); return NextResponse.redirect( new URL(`/verify/error?${params.toString()}`, request.url), ); } // Process the verification token await nhost.auth.refreshToken({ refreshToken }); // Redirect to profile on successful verification return NextResponse.redirect(new URL("/profile", request.url)); } catch (err) { const error = err as FetchError<ErrorResponse>; const errorMessage = `Failed to verify token: ${error.message}`; // Collect all query parameters const params = new URLSearchParams(request.nextUrl.searchParams); params.set("message", errorMessage); return NextResponse.redirect( new URL(`/verify/error?${params.toString()}`, request.url), ); }}
The verification error page is a server component that displays helpful error messages and debugging information when email verification fails. It shows the specific error message and any URL parameters that might help diagnose the issue.
src/app/verify/error/page.tsx
Copy
Ask AI
import Link from "next/link";export default async function VerifyError({ searchParams,}: { searchParams: Promise<Record<string, string>>;}) { const params = await searchParams; const message = params?.message || "Unknown verification error"; // Filter out the message to show other URL parameters const urlParams = Object.entries(params).filter(([key]) => key !== "message"); return ( <div> <h1>Email Verification</h1> <div className="page-center"> <p className="verification-status error">Verification failed</p> <p className="margin-bottom">{message}</p> {urlParams.length > 0 && ( <div className="debug-panel"> <p className="debug-title">URL Parameters:</p> {urlParams.map(([key, value]) => ( <div key={key} className="debug-item"> <span className="debug-key">{key}:</span>{" "} <span className="debug-value">{value}</span> </div> ))} </div> )} <Link href="/signin" className="auth-button secondary"> Back to Sign In </Link> </div> </div> );}
Important Configuration Required: Before testing email verification, you must configure your Nhost project’s authentication settings:
Go to your Nhost project dashboard
Navigate to Settings → Authentication
Add your local development URL (e.g., http://localhost:3000) to the Allowed Redirect URLs field
Ensure your production domain is also added when deploying
Without this configuration, you’ll receive a redirectTo not allowed error when users attempt to sign up or verify their email addresses.
In this step, we’ll implement user sign-out functionality using Next.js patterns. We’ll create a client component for the sign-out button and a server action to handle the actual sign-out process securely on the server side.
Sign Out Button
Server Action
The sign-out button is a client component that provides an interactive button for users to sign out. It handles the user interaction and calls the server action, then manages navigation and component refresh after sign-out.
The sign-out server action handles the authentication logic securely on the server side. It retrieves the current session, calls Nhost’s sign-out method with the refresh token, and redirects the user to the home page after successful sign-out.
In this step, we’ll update the server-side navigation component that shows different links based on the user’s authentication state. The navigation will display “Sign In” and “Sign Up” links for unauthenticated users, and “Profile” and “Sign Out” for authenticated users.
src/components/Navigation.tsx
Copy
Ask AI
import Link from "next/link";import { createNhostClient } from "../lib/nhost/server";import SignOutButton from "./SignOutButton";export default async function Navigation() { const nhost = await createNhostClient(); const session = nhost.getUserSession(); return ( <nav className="navigation"> <div className="nav-container"> <Link href="/" className="nav-logo"> Nhost Next.js Demo </Link> <div className="nav-links"> <Link href="/" className="nav-link"> Home </Link> {session ? ( <> <Link href="/profile" className="nav-link"> Profile </Link> <SignOutButton /> </> ) : ( <> <Link href="/signin" className="nav-link"> Sign In </Link> <Link href="/signup" className="nav-link"> Sign Up </Link> </> )} </div> </div> </nav> );}
In this step, we’ll configure the middleware to allow access to authentication-related routes without requiring authentication. This ensures that users can access sign-in, sign-up, and email verification pages even when not logged in.
src/middleware.ts
Copy
Ask AI
import type { NextRequest } from "next/server";import { NextResponse } from "next/server";import { handleNhostMiddleware } from "./lib/nhost/server";// Define public routes that don't require authenticationconst publicRoutes = ["/", "/signin", "/signup", "/verify", "/verify/error"];export async function middleware(request: NextRequest) { // Create a response that we'll modify as needed const response = NextResponse.next(); // Get the current path const path = request.nextUrl.pathname; // Check if this is a public route or a public asset const isPublicRoute = publicRoutes.some( (route) => path === route || path.startsWith(`${route}/`), ); // Handle Nhost authentication and token refresh // Always call this to ensure session is up-to-date // even for public routes, so that session changes are detected const session = await handleNhostMiddleware(request, response); // If it's a public route, allow access without checking auth if (isPublicRoute) { return response; } // If no session and not a public route, redirect to signin if (!session) { const homeUrl = new URL("/", request.url); return NextResponse.redirect(homeUrl); } // Session exists, allow access to protected route return response;}// Define which routes this middleware should run onexport const config = { matcher: [ /* * Match all request paths except: * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - public files (public directory) */ "/((?!_next/static|_next/image|favicon.ico|public).*)", ],};
Start your Next.js development server and test the complete authentication flow to ensure everything works properly.
Copy
Ask AI
npm run dev
Things to try out:
Email Verification Flow: Try signing up with a new email address. Check your email for the verification link and click it. The verification route handler will process the token and redirect you to your profile.
Sign In/Out Flow: Try signing out and then signing back in with the same credentials using the server actions.
Server-Side Navigation: Notice how navigation links change based on authentication state - the navigation component is rendered server-side and shows different content based on the session.
Route Protection: Try accessing protected routes while logged out to see the middleware-based protection in action.
Cross-Tab Consistency: Open multiple tabs and test signing out from one tab. Unlike client-side React apps, you’ll need to refresh or navigate to see changes in other tabs due to server-side rendering.