This tutorial part demonstrates how to implement robust route protection in a Next.js application using Nhost authentication. You’ll build a complete authentication system with a protected /profile page that includes server-side rendering, client components, server actions and API routes. It also features cross-tab session synchronization, and automatic redirects. In addition, we will see how to show conditional navigation and content based on authentication status.
This is Part 2 in the Full-Stack Next.js Development with Nhost series. This part builds a foundation for authentication-protected routes that you can extend to secure any part of your application.

Full-Stack Next.js Development with Nhost

Prerequisites

  • An Nhost project set up
  • Node.js 20+ installed
  • Basic knowledge of React and Next.js

Step-by-Step Guide

1

Create a New Next.js App

We’ll start by creating a fresh Next.js application with TypeScript support. Next.js provides server-side rendering, file-based routing, and optimized builds for modern React applications.
npx create-next-app@15 nhost-nextjs-tutorial --typescript --eslint --app --no-tailwind --yes
cd nhost-nextjs-tutorial
npm install
2

Install Required Dependencies

Install the Nhost JavaScript SDK for authentication and session management. The Nhost SDK handles authentication with built-in Next.js support for server-side rendering.
npm install @nhost/nhost-js
3

Environment Configuration

Configure your Nhost project connection by creating environment variables. This allows the app to connect to your specific Nhost backend from both server and client sides.Create a .env.local file in your project root:
NHOST_REGION=<region>
NHOST_SUBDOMAIN=<subdomain>
Replace <region> and <subdomain> with the actual values from your Nhost project dashboard.
4

Create Server-Side Nhost Helper

Create server-side utilities for handling authentication in Next.js server components and middleware. This enables server-side session access and token refresh functionality.
src/lib/nhost/server.tsx
import { createServerClient, type NhostClient } from "@nhost/nhost-js";
import { DEFAULT_SESSION_KEY, type Session } from "@nhost/nhost-js/session";
import { cookies } from "next/headers";
import type { NextRequest, NextResponse } from "next/server";

const key = DEFAULT_SESSION_KEY;

/**
 * Creates an Nhost client for use in server components.
 *
 * We rely on the vanilla createClient method from the Nhost JS SDK and a SessionStorage
 * customized to be able to retrieve the session from cookies in Next.js server components.
 */
export async function createNhostClient(): Promise<NhostClient> {
  const cookieStore = await cookies();

  const nhost = createServerClient({
    region: process.env["NHOST_REGION"] || "local",
    subdomain: process.env["NHOST_SUBDOMAIN"] || "local",
    storage: {
      // storage compatible with Next.js server components
      get: (): Session | null => {
        const s = cookieStore.get(key)?.value || null;
        if (!s) {
          return null;
        }
        const session = JSON.parse(s) as Session;
        return session;
      },
      set: (value: Session) => {
        cookieStore.set(key, JSON.stringify(value));
      },
      remove: () => {
        cookieStore.delete(key);
      },
    },
  });

  return nhost;
}

/**
 * Middleware function to handle Nhost authentication and session management.
 *
 * This function is designed to be used in Next.js middleware to manage user sessions
 * and refresh tokens. Refreshing the session needs to be done in the middleware
 * to ensure that the session is always up-to-date an accessible by both server and client components.
 *
 * @param {NextRequest} request - The incoming Next.js request object
 * @param {NextResponse} response - The outgoing Next.js response object
 */
export async function handleNhostMiddleware(
  request: NextRequest,
  response: NextResponse<unknown>,
): Promise<Session | null> {
  const nhost = createServerClient({
    region: process.env["NHOST_REGION"] || "local",
    subdomain: process.env["NHOST_SUBDOMAIN"] || "local",
    storage: {
      // storage compatible with Next.js middleware
      get: (): Session | null => {
        const raw = request.cookies.get(key)?.value || null;
        if (!raw) {
          return null;
        }
        const session = JSON.parse(raw) as Session;
        return session;
      },
      set: (value: Session) => {
        response.cookies.set({
          name: key,
          value: JSON.stringify(value),
          path: "/",
          httpOnly: false, //if set to true we can't access it in the client
          secure: process.env.NODE_ENV === "production",
          sameSite: "lax",
          maxAge: 60 * 60 * 24 * 30, // 30 days in seconds
        });
      },
      remove: () => {
        response.cookies.delete(key);
      },
    },
  });

  // we only want to refresh the session if  the token will
  // expire in the next 60 seconds
  return await nhost.refreshSession(60);
}
5

Create Middleware for Route Protection

Create Next.js middleware to handle route protection at the server level. This middleware runs before any page renders and automatically redirects unauthenticated users from protected routes.
src/middleware.ts
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { handleNhostMiddleware } from "./lib/nhost/server";

// Define public routes that don't require authentication
const publicRoutes = ["/"];

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 on
export 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).*)",
  ],
};
6

Create the Protected Profile Page

Create a page that displays user information. Note that the page itself doesn’t need any special authentication logic - the route protection is handled entirely by the middleware from the previous step.
src/app/profile/page.tsx
import { createNhostClient } from "../../lib/nhost/server";

export default async function Profile() {
  // Create the client with async cookie access
  const nhost = await createNhostClient();
  const session = nhost.getUserSession();

  return (
    <div className="container">
      <header className="page-header">
        <h1 className="page-title">Your Profile</h1>
      </header>

      <div className="form-card">
        <h3 className="form-title">User Information</h3>
        <div className="form-fields">
          <div className="field-group">
            <strong>Display Name:</strong>{" "}
            {session?.user?.displayName || "Not set"}
          </div>
          <div className="field-group">
            <strong>Email:</strong> {session?.user?.email || "Not available"}
          </div>
          <div className="field-group">
            <strong>User ID:</strong> {session?.user?.id || "Not available"}
          </div>
          <div className="field-group">
            <strong>Roles:</strong> {session?.user?.roles?.join(", ") || "None"}
          </div>
          <div className="field-group">
            <strong>Email Verified:</strong>
            <span
              className={
                session?.user?.emailVerified
                  ? "email-verified"
                  : "email-unverified"
              }
            >
              {session?.user?.emailVerified ? "✓ Yes" : "✗ No"}
            </span>
          </div>
        </div>
      </div>

      <div className="form-card">
        <h3 className="form-title">Session Information</h3>
        <div className="description">
          <pre className="session-display">
            {JSON.stringify(session, null, 2)}
          </pre>
        </div>
      </div>
    </div>
  );
}
7

Create a Simple Home Page

Build a public homepage that adapts its content based on authentication status. This shows users different options depending on whether they’re signed in.
src/app/page.tsx
import { createNhostClient } from "../lib/nhost/server";

export default async function Home() {
  const nhost = await createNhostClient();
  const session = nhost.getUserSession();

  return (
    <div className="container">
      <header className="page-header">
        <h1 className="page-title">Welcome to Nhost Next.js Demo</h1>
      </header>

      {session ? (
        <div>
          <p>Hello, {session.user?.displayName || session.user?.email}!</p>
        </div>
      ) : (
        <div>
          <p>You are not signed in.</p>
        </div>
      )}
    </div>
  );
}
8

Create the Navigation Component

Create a server-side navigation component that provides consistent navigation across all pages. This component adapts its links based on authentication status - showing different options for signed-in and signed-out users with server-side rendering.
src/components/Navigation.tsx
import Link from "next/link";
import { createNhostClient } from "../lib/nhost/server";

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>
            </>
          ) : (
            <>
              Placeholder for signin/signup links
            </>
          )}
        </div>
      </div>
    </nav>
  );
}
9

Update the Root Layout

Configure the Next.js root layout to include the navigation and page content.
src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import Navigation from "../components/Navigation";

export const metadata: Metadata = {
  title: "Nhost Next.js Tutorial",
  description: "Next.js tutorial with Nhost authentication",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Navigation />
        <div className="app-content">{children}</div>
      </body>
    </html>
  );
}
10

Add Application Styles

Replace the contents of the file src/app/global.css with the following styles to provide a clean and modern look for the application. This file will be used across the rest of series.
src/app/globals.css
:root {
  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
  line-height: 1.5;
  font-weight: 400;

  color-scheme: dark;
  color: rgba(255, 255, 255, 0.87);
  background-color: #242424;

  font-synthesis: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body {
  margin: 0;
  min-width: 320px;
  min-height: 100vh;
}

#root {
  width: 100%;
  min-height: 100vh;
  display: block;
  margin: 0;
  padding: 0;
}

a {
  font-weight: 500;
  color: #646cff;
  text-decoration: inherit;
}

a:hover {
  color: #535bf2;
}

h1 {
  font-size: 3.2em;
  line-height: 1.1;
}

button {
  border-radius: 8px;
  border: 1px solid transparent;
  padding: 0.6em 1.2em;
  font-size: 1em;
  font-weight: 500;
  font-family: inherit;
  background-color: #1a1a1a;
  cursor: pointer;
  transition: border-color 0.25s;
}

button:hover {
  border-color: #646cff;
}

button:focus,
button:focus-visible {
  outline: 4px auto -webkit-focus-ring-color;
}

input,
textarea {
  width: 100%;
  padding: 0.875rem 1rem;
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: 8px;
  font-size: 0.875rem;
  transition: all 0.2s ease;
  background: rgba(255, 255, 255, 0.05);
  color: white;
  box-sizing: border-box;
  font-family: inherit;
}

input:focus,
textarea:focus {
  outline: none;
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
  background: rgba(255, 255, 255, 0.08);
}

input::placeholder,
textarea::placeholder {
  color: rgba(255, 255, 255, 0.5);
}

textarea {
  resize: vertical;
  min-height: 4rem;
}

label {
  display: block;
  margin: 0 0 0.5rem 0;
  font-weight: 500;
  color: rgba(255, 255, 255, 0.9);
  font-size: 0.875rem;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

/* Global Layout */
.app-content {
  padding: 0 2rem 2rem;
  max-width: 800px;
  margin: 0 auto;
}

.page-center {
  text-align: center;
  padding: 2rem;
}

.page-header {
  margin-bottom: 2rem;
}

.page-title {
  font-weight: 700;
  margin: 0;
  display: flex;
  align-items: center;
  gap: 1rem;
}

.margin-bottom {
  margin-bottom: 1rem;
}

.margin-top {
  margin-top: 1rem;
}

.container {
  width: 800px;
  max-width: calc(100vw - 4rem);
  min-width: 320px;
  margin: 0 auto;
  padding: 2rem;
  box-sizing: border-box;
  position: relative;
}

/* Status Messages */
.success-message {
  padding: 1rem;
  background-color: #d4edda;
  color: #155724;
  border-radius: 8px;
  margin-bottom: 1rem;
}

.error-message {
  background: rgba(239, 68, 68, 0.1);
  color: #ef4444;
  border: 1px solid rgba(239, 68, 68, 0.3);
  border-radius: 12px;
  padding: 1rem 1.5rem;
  margin: 1rem 0;
}

.help-text {
  color: #666;
}

.verification-status {
  color: #28a745;
  font-size: 1.2rem;
  font-weight: bold;
  margin-bottom: 1rem;
}

.verification-status.error {
  color: #dc3545;
  font-size: 1.1rem;
}

/* Email Verification Status */
.email-verified {
  color: #10b981;
  font-weight: bold;
  margin-left: 0.5rem;
}

.email-unverified {
  color: #ef4444;
  font-weight: bold;
  margin-left: 0.5rem;
}

/* Debug Info */
.debug-panel {
  margin-bottom: 1rem;
  padding: 1rem;
  background-color: #f8f9fa;
  border-radius: 8px;
  text-align: left;
  max-height: 200px;
  overflow: auto;
}

.debug-title {
  font-weight: bold;
  margin-bottom: 0.5rem;
}

.debug-item {
  margin-bottom: 0.25rem;
}

.debug-key {
  font-family: monospace;
  color: #007bff;
}

.debug-value {
  font-family: monospace;
}

/* Session Display */
.session-display {
  font-size: 0.75rem;
  overflow: auto;
  margin: 0;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
}

/* Loading Spinner */
.spinner-verify {
  width: 32px;
  height: 32px;
  border: 3px solid #f3f3f3;
  border-top: 3px solid #007bff;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin: 0 auto;
}

/* Navigation */
.navigation {
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  position: sticky;
  top: 0;
  z-index: 100;
  margin-bottom: 2rem;
}

.nav-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 1rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.nav-logo {
  font-size: 1.25rem;
  font-weight: 700;
  color: white;
  text-decoration: none;
  background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.nav-logo:hover {
  opacity: 0.8;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.nav-link {
  color: rgba(255, 255, 255, 0.8);
  text-decoration: none;
  font-weight: 500;
  font-size: 0.875rem;
  padding: 0.5rem 0.75rem;
  border-radius: 6px;
  transition: all 0.2s ease;
  border: none;
  background: none;
  cursor: pointer;
  font-family: inherit;
}

.nav-link:hover {
  color: white;
  background: rgba(255, 255, 255, 0.1);
}

.nav-button {
  color: #ef4444;
}

.nav-button:hover {
  background: rgba(239, 68, 68, 0.2);
}

/* Buttons */
.btn {
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 8px;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.2s ease;
  font-size: 0.875rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-width: 120px;
}

.btn-primary {
  background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
  color: white;
}

.btn-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}

.btn-secondary {
  background: rgba(255, 255, 255, 0.1);
  color: white;
  border: 1px solid rgba(255, 255, 255, 0.2);
}

.btn-secondary:hover {
  background: rgba(255, 255, 255, 0.2);
}

.btn-cancel {
  background: rgba(239, 68, 68, 0.1);
  color: #ef4444;
  border: 1px solid rgba(239, 68, 68, 0.3);
}

.btn-cancel:hover {
  background: rgba(239, 68, 68, 0.2);
}

/* Loading State */
.loading-container {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 4rem 2rem;
}

.loading-content {
  display: flex;
  align-items: center;
  gap: 1rem;
}

.spinner {
  width: 2rem;
  height: 2rem;
  border: 3px solid rgba(59, 130, 246, 0.3);
  border-top: 3px solid #3b82f6;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.loading-text {
  color: rgba(255, 255, 255, 0.7);
  font-size: 0.875rem;
}

/* Empty State */
.empty-state {
  text-align: center;
  padding: 4rem 2rem;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
}

.empty-icon {
  width: 4rem;
  height: 4rem;
  color: rgba(255, 255, 255, 0.4);
  margin: 0 auto 1rem;
}

.empty-title {
  font-size: 1.25rem;
  font-weight: 600;
  color: white;
  margin: 0 0 0.5rem 0;
}

.empty-description {
  color: rgba(255, 255, 255, 0.6);
  margin: 0;
}

/* Forms */
.form-card {
  background: rgba(255, 255, 255, 0.05);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 16px;
  padding: 2rem;
  margin-bottom: 2rem;
  width: 100%;
  box-sizing: border-box;
}

.form-title {
  font-size: 1.5rem;
  font-weight: 600;
  color: white;
  margin: 0 0 1.5rem 0;
}

.form-fields {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.field-group {
  display: flex;
  flex-direction: column;
}

.form-actions {
  display: flex;
  gap: 1rem;
  margin-top: 1rem;
}

/* Auth Pages */
.auth-form {
  max-width: 400px;
}

.auth-form-field {
  margin-bottom: 1rem;
}

.auth-input {
  width: 100%;
  padding: 0.5rem;
  margin-top: 0.25rem;
}

.auth-error {
  color: red;
  margin-bottom: 1rem;
  padding: 0.5rem;
  background-color: #fee;
  border-radius: 4px;
}

.auth-button {
  width: 100%;
  padding: 0.75rem;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.auth-button:disabled {
  cursor: not-allowed;
}

.auth-button.primary {
  background-color: #28a745;
}

.auth-button.secondary {
  background-color: #007bff;
}

.auth-links {
  margin-top: 1rem;
}

/* Todos */

.todo-form {
  width: 100%;
}

/* Todo List */
.todos-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.todo-card {
  background: rgba(255, 255, 255, 0.03);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  transition: all 0.2s ease;
  overflow: hidden;
  width: 100%;
  box-sizing: border-box;
}

.todo-card:hover {
  border-color: rgba(255, 255, 255, 0.2);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}

.todo-card.completed {
  opacity: 0.7;
}

/* Todo Content */
.todo-content {
  padding: 1rem 1.5rem;
}

.todo-edit {
  padding: 1.5rem;
  min-height: 200px;
}

.edit-fields {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.edit-actions {
  display: flex;
  gap: 1rem;
  margin-top: 1.5rem;
}

.todo-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

.todo-title-btn {
  background: none;
  border: none;
  padding: 0;
  text-align: left;
  font-size: 1.25rem;
  font-weight: 600;
  color: white;
  cursor: pointer;
  transition: color 0.2s ease;
  flex: 1;
  line-height: 1.4;
  word-wrap: break-word;
  overflow-wrap: break-word;
  max-width: calc(100% - 140px);
}

.todo-title-btn:hover {
  color: #3b82f6;
}

.todo-title-btn.completed {
  text-decoration: line-through;
  color: rgba(255, 255, 255, 0.5);
}

.todo-actions {
  display: flex;
  gap: 0.5rem;
  flex-shrink: 0;
  min-width: 132px;
  justify-content: flex-end;
}

/* Action Buttons */
.action-btn {
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.05);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 18px;
  transition: all 0.2s ease;
  -webkit-text-fill-color: currentColor;
}

.action-btn-complete {
  color: #10b981;
  font-size: 20px;
}

.action-btn-complete:hover {
  background: rgba(16, 185, 129, 0.2);
  color: #34d399;
}

.action-btn-edit {
  color: #3b82f6;
}

.action-btn-edit:hover {
  background: rgba(59, 130, 246, 0.2);
  color: #60a5fa;
}

.action-btn-delete {
  color: #ef4444;
}

.action-btn-delete:hover {
  background: rgba(239, 68, 68, 0.2);
  color: #f87171;
}

/* Add Todo Button */
.add-todo-btn {
  width: 36px;
  height: 36px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 18px;
  font-weight: normal;
  -webkit-text-fill-color: white;
  transition: all 0.2s ease;
}

.add-todo-btn:hover {
  transform: scale(1.1);
  box-shadow: 0 4px 20px rgba(59, 130, 246, 0.4);
}

/* Todo Details */
.todo-details {
  margin-top: 1rem;
  padding-top: 1rem;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.description {
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 8px;
  padding: 1rem;
  margin-bottom: 1rem;
}

.description p {
  margin: 0;
  color: rgba(255, 255, 255, 0.8);
  line-height: 1.6;
}

.description.completed p {
  text-decoration: line-through;
  color: rgba(255, 255, 255, 0.5);
}

.todo-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
}

.meta-dates {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
}

.meta-item {
  font-size: 0.75rem;
  color: rgba(255, 255, 255, 0.5);
}

.completion-badge {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.75rem;
  color: #10b981;
  font-weight: 500;
}

.completion-icon {
  width: 0.875rem;
  height: 0.875rem;
}

/* Responsive Design */
@media (max-width: 768px) {
  .nav-container {
    padding: 1rem;
    flex-direction: column;
    gap: 1rem;
  }

  .nav-links {
    gap: 1rem;
    flex-wrap: wrap;
    justify-content: center;
  }

  .container {
    padding: 1rem;
  }

  .form-actions {
    flex-direction: column;
  }

  .edit-actions {
    flex-direction: column;
  }

  .todo-header {
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
  }

  .todo-actions {
    align-self: stretch;
    justify-content: center;
  }

  .meta-dates {
    flex-direction: column;
    gap: 0.25rem;
  }

  .todo-meta {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }
}

/* File Upload */
.file-upload-btn {
  min-height: 120px;
  flex-direction: column;
  gap: 0.5rem;
  width: 100%;
  border: 2px dashed rgba(255, 255, 255, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.2s ease;
}

.file-upload-btn:hover {
  border-color: rgba(255, 255, 255, 0.5);
  background-color: rgba(255, 255, 255, 0.05);
}

.file-upload-info {
  margin-top: 0.5rem;
  font-size: 0.875rem;
  color: rgba(255, 255, 255, 0.8);
}

/* File Table */
.file-table {
  width: 100%;
  border-collapse: collapse;
}

.file-table th {
  padding: 0.75rem;
  text-align: left;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.7);
  font-weight: 500;
  font-size: 0.875rem;
}

.file-table th:last-child {
  text-align: center;
}

.file-table td {
  padding: 0.75rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}

.file-table tr:hover {
  background-color: rgba(255, 255, 255, 0.02);
}

.file-name {
  color: white;
  font-weight: 500;
}

.file-meta {
  color: rgba(255, 255, 255, 0.6);
  font-size: 0.875rem;
}

.file-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: center;
}

/* Responsive File Table */
@media (max-width: 768px) {
  .file-table {
    font-size: 0.875rem;
  }

  .file-table th,
  .file-table td {
    padding: 0.5rem;
  }

  .file-actions {
    flex-direction: column;
    gap: 0.25rem;
  }
}
11

Run and test the Application

Start the development server to test your route protection implementation:
npm run dev
Things to try out:
  1. Try navigating to /profile - you should be redirected to the homepage / since you’re not authenticated.
  2. Because you are not signed in, the navigation bar should only show the “Home” link and the placeholder for signin/signup links.
  3. On the homepage, you should see a message indicating that you are not signed in.
After we complete the next tutorial on user authentication, you will be able to sign in and access the protected /profile page and see how the navigation bar and homepage updates accordingly.

How It Works

  1. Server-Side Helpers: Utilities for handling authentication in Next.js server components and middleware
  2. Middleware Route Protection: Next.js middleware runs before any page renders, automatically redirecting unauthenticated users from protected routes and refreshing tokens
  3. AuthProvider: Client-side provider that manages authentication state using Nhost’s client with cookie-based storage for server/client synchronization
  4. Protected Pages: Server components can assume authentication since middleware handles protection, focusing purely on rendering authenticated content
  5. Navigation: Server-side navigation component that adapts its links based on authentication status
  6. Automatic Redirects: All route protection and redirects are handled at the middleware level for optimal performance and security

Key Features Demonstrated