Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages

The Dashboard Every SaaS Needs

Your landing page converts. Your Stripe checkout works. But users sign up and then what? They see a blank screen.

A production user dashboard needs: protected routes, profile management, usage stats, billing m…


This content originally appeared on DEV Community and was authored by Atlas Whoff

The Dashboard Every SaaS Needs

Your landing page converts. Your Stripe checkout works. But users sign up and then what? They see a blank screen.

A production user dashboard needs: protected routes, profile management, usage stats, billing management, and a clean nav. Here's how to build it in Next.js 14.

Route Structure

app/
  dashboard/
    layout.tsx          # Auth check + sidebar
    page.tsx            # Overview/home
    profile/page.tsx    # User settings
    billing/page.tsx    # Stripe portal
    settings/page.tsx   # App preferences

Auth Guard in Layout

The dashboard layout runs the auth check once for all child routes:

// app/dashboard/layout.tsx
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { redirect } from 'next/navigation'
import DashboardNav from '@/components/DashboardNav'

export default async function DashboardLayout({
  children
}: {
  children: React.ReactNode
}) {
  const session = await getServerSession(authOptions)

  if (!session) {
    redirect('/login')
  }

  return (
    <div className="flex h-screen">
      <DashboardNav user={session.user} />
      <main className="flex-1 overflow-auto p-8">
        {children}
      </main>
    </div>
  )
}

Dashboard Overview Page

// app/dashboard/page.tsx
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { db } from '@/lib/db'
import StatsCard from '@/components/StatsCard'

export default async function DashboardPage() {
  const session = await getServerSession(authOptions)
  const user = await db.user.findUnique({
    where: { email: session!.user!.email! },
    include: { subscription: true, _count: { select: { apiCalls: true } } }
  })

  return (
    <div>
      <h1 className="text-2xl font-bold mb-6">
        Welcome back, {session?.user?.name?.split(' ')[0]}
      </h1>

      <div className="grid grid-cols-3 gap-6 mb-8">
        <StatsCard
          title="Plan"
          value={user?.subscription?.plan ?? 'Free'}
        />
        <StatsCard
          title="API Calls"
          value={user?._count.apiCalls ?? 0}
        />
        <StatsCard
          title="Member Since"
          value={user?.createdAt.toLocaleDateString()}
        />
      </div>
    </div>
  )
}

Sidebar Navigation

// components/DashboardNav.tsx
'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { signOut } from 'next-auth/react'
import { LayoutDashboard, User, CreditCard, Settings, LogOut } from 'lucide-react'

const navItems = [
  { href: '/dashboard', label: 'Overview', icon: LayoutDashboard },
  { href: '/dashboard/profile', label: 'Profile', icon: User },
  { href: '/dashboard/billing', label: 'Billing', icon: CreditCard },
  { href: '/dashboard/settings', label: 'Settings', icon: Settings },
]

export default function DashboardNav({ user }: { user: any }) {
  const pathname = usePathname()

  return (
    <nav className="w-64 bg-gray-900 border-r border-gray-800 flex flex-col">
      <div className="p-6 border-b border-gray-800">
        <p className="text-sm text-gray-400">{user.email}</p>
      </div>

      <div className="flex-1 p-4">
        {navItems.map(({ href, label, icon: Icon }) => (
          <Link
            key={href}
            href={href}
            className={`flex items-center gap-3 px-3 py-2 rounded-lg mb-1 text-sm
              ${ pathname === href
                ? 'bg-blue-600 text-white'
                : 'text-gray-400 hover:bg-gray-800 hover:text-white'
              }`}
          >
            <Icon size={16} />
            {label}
          </Link>
        ))}
      </div>

      <div className="p-4 border-t border-gray-800">
        <button
          onClick={() => signOut()}
          className="flex items-center gap-3 px-3 py-2 text-sm text-gray-400 hover:text-white w-full"
        >
          <LogOut size={16} />
          Sign out
        </button>
      </div>
    </nav>
  )
}

Billing Page with Stripe Portal

// app/dashboard/billing/page.tsx
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { db } from '@/lib/db'
import { stripe } from '@/lib/stripe'
import ManageBillingButton from './ManageBillingButton'

export default async function BillingPage() {
  const session = await getServerSession(authOptions)
  const user = await db.user.findUnique({
    where: { email: session!.user!.email! },
    include: { subscription: true }
  })

  const plan = user?.subscription?.plan ?? 'Free'
  const nextBillDate = user?.subscription?.currentPeriodEnd

  return (
    <div>
      <h1 className="text-2xl font-bold mb-6">Billing</h1>

      <div className="bg-gray-800 rounded-lg p-6 mb-6">
        <div className="flex justify-between items-center">
          <div>
            <p className="text-sm text-gray-400">Current Plan</p>
            <p className="text-xl font-semibold">{plan}</p>
            {nextBillDate && (
              <p className="text-sm text-gray-400 mt-1">
                Next billing: {nextBillDate.toLocaleDateString()}
              </p>
            )}
          </div>
          <ManageBillingButton />
        </div>
      </div>
    </div>
  )
}

Manage Billing Button (Server Action)

// app/dashboard/billing/ManageBillingButton.tsx
'use client'

import { useState } from 'react'

export default function ManageBillingButton() {
  const [loading, setLoading] = useState(false)

  const handleManageBilling = async () => {
    setLoading(true)
    const res = await fetch('/api/billing/portal', { method: 'POST' })
    const { url } = await res.json()
    window.location.href = url
  }

  return (
    <button
      onClick={handleManageBilling}
      disabled={loading}
      className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm"
    >
      {loading ? 'Loading...' : 'Manage Billing'}
    </button>
  )
}

Stripe Portal API Route

// app/api/billing/portal/route.ts
import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { stripe } from '@/lib/stripe'
import { db } from '@/lib/db'

export async function POST() {
  const session = await getServerSession(authOptions)
  if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const user = await db.user.findUnique({
    where: { email: session.user!.email! }
  })

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user!.stripeCustomerId!,
    return_url: `${process.env.NEXTAUTH_URL}/dashboard/billing`
  })

  return NextResponse.json({ url: portalSession.url })
}

Profile Page

// app/dashboard/profile/page.tsx
'use client'

import { useSession } from 'next-auth/react'
import { useState } from 'react'

export default function ProfilePage() {
  const { data: session } = useSession()
  const [name, setName] = useState(session?.user?.name ?? '')
  const [saving, setSaving] = useState(false)

  const handleSave = async () => {
    setSaving(true)
    await fetch('/api/user/profile', {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name })
    })
    setSaving(false)
  }

  return (
    <div>
      <h1 className="text-2xl font-bold mb-6">Profile</h1>
      <div className="max-w-md space-y-4">
        <div>
          <label className="text-sm text-gray-400">Name</label>
          <input
            value={name}
            onChange={e => setName(e.target.value)}
            className="w-full mt-1 px-3 py-2 bg-gray-800 rounded-lg"
          />
        </div>
        <div>
          <label className="text-sm text-gray-400">Email</label>
          <p className="mt-1 text-gray-300">{session?.user?.email}</p>
        </div>
        <button
          onClick={handleSave}
          disabled={saving}
          className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-sm"
        >
          {saving ? 'Saving...' : 'Save Changes'}
        </button>
      </div>
    </div>
  )
}

This is Already Built

The AI SaaS Starter Kit ships with all of this pre-configured:

  • Dashboard layout with auth guard
  • Sidebar nav with active state
  • Profile page with edit functionality
  • Billing page with Stripe Portal integration
  • Settings page
  • Stats cards component
  • All routes protected

$99 one-time at whoffagents.com

Skip the 2 days of dashboard wiring. It's done.


This content originally appeared on DEV Community and was authored by Atlas Whoff


Print Share Comment Cite Upload Translate Updates
APA

Atlas Whoff | Sciencx (2026-04-07T07:17:38+00:00) Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages. Retrieved from https://www.scien.cx/2026/04/07/building-a-user-dashboard-in-next-js-14-auth-billing-and-profile-pages/

MLA
" » Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages." Atlas Whoff | Sciencx - Tuesday April 7, 2026, https://www.scien.cx/2026/04/07/building-a-user-dashboard-in-next-js-14-auth-billing-and-profile-pages/
HARVARD
Atlas Whoff | Sciencx Tuesday April 7, 2026 » Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages., viewed ,<https://www.scien.cx/2026/04/07/building-a-user-dashboard-in-next-js-14-auth-billing-and-profile-pages/>
VANCOUVER
Atlas Whoff | Sciencx - » Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2026/04/07/building-a-user-dashboard-in-next-js-14-auth-billing-and-profile-pages/
CHICAGO
" » Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages." Atlas Whoff | Sciencx - Accessed . https://www.scien.cx/2026/04/07/building-a-user-dashboard-in-next-js-14-auth-billing-and-profile-pages/
IEEE
" » Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages." Atlas Whoff | Sciencx [Online]. Available: https://www.scien.cx/2026/04/07/building-a-user-dashboard-in-next-js-14-auth-billing-and-profile-pages/. [Accessed: ]
rf:citation
» Building a User Dashboard in Next.js 14: Auth, Billing, and Profile Pages | Atlas Whoff | Sciencx | https://www.scien.cx/2026/04/07/building-a-user-dashboard-in-next-js-14-auth-billing-and-profile-pages/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.