User Management
Learn how to manage users, sessions, and authentication in your SaaS application
The SaaS starter kit uses Auth.js v5 with Prisma for comprehensive user management. This guide covers user operations, session handling, and authentication flows.
Prerequisites
- Understanding of the Database Schema
- Familiarity with Next.js Server Components
- Basic knowledge of Prisma queries
Overview
User management encompasses:
- User Accounts: Profile data, email, name, and image
- OAuth Accounts: Connected providers (GitHub, Google)
- Sessions: Active user sessions managed by Auth.js
- Subscriptions: Billing plan and status per user
- User Data: Personal todos and user-generated content
Getting Current User
Server Components (Recommended)
In Server Components, use the auth() function from Auth.js:
1import { auth } from "@/lib/auth";2import { redirect } from "next/navigation";3import Image from "next/image";4 5export default async function DashboardPage() {6 const session = await auth();7 8 // Redirect if not authenticated9 if (!session?.user) {10 redirect("/login");11 }12 13 return (14 <div>15 <h1>Welcome, {session.user.name}!</h1>16 <p>Email: {session.user.email}</p>17 {session.user.image && (18 <Image19 src={session.user.image}20 alt="Profile"21 width={64}22 height={64}23 className="rounded-full"24 />25 )}26 </div>27 );28}The session object contains:
user.id- User ID (CUID)user.email- Email addressuser.name- Display nameuser.image- Profile picture URL
Client Components
For client-side access, fetch the session via API:
1"use client";2 3import { useEffect, useState } from "react";4 5export function UserProfile() {6 const [session, setSession] = useState(null);7 8 useEffect(() => {9 fetch("/api/auth/session")10 .then((res) => res.json())11 .then((data) => setSession(data));12 }, []);13 14 if (!session?.user) {15 return <div>Not logged in</div>;16 }17 18 return (19 <div>20 <p>Welcome, {session.user.name}!</p>21 </div>22 );23}API Routes
In API routes, use auth() to check authentication:
1import { auth } from "@/lib/auth";2import { NextResponse } from "next/server";3 4export async function GET() {5 const session = await auth();6 7 if (!session?.user) {8 return NextResponse.json(9 { error: "Unauthorized" },10 { status: 401 }11 );12 }13 14 // User is authenticated15 return NextResponse.json({16 userId: session.user.id,17 email: session.user.email,18 });19}User Database Operations
Fetch User with Relations
Get complete user data including subscription, accounts, and todos:
1import { auth } from "@/lib/auth";2import { prisma } from "@/lib/db";3import { NextResponse } from "next/server";4 5export async function GET() {6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });9 }10 11 const user = await prisma.user.findUnique({12 where: { id: session.user.id },13 include: {14 subscription: true, // Include billing info15 accounts: true, // Include OAuth accounts16 todos: {17 orderBy: { createdAt: "desc" },18 take: 10, // Get 10 most recent todos19 },20 },21 });22 23 return NextResponse.json(user);24}Update User Profile
1import { auth } from "@/lib/auth";2import { prisma } from "@/lib/db";3import { NextResponse } from "next/server";4 5export async function PATCH(request: Request) {6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });9 }10 11 const { name } = await request.json();12 13 const updatedUser = await prisma.user.update({14 where: { id: session.user.id },15 data: { name },16 });17 18 return NextResponse.json(updatedUser);19}List All Users (Admin Only)
1import { auth } from "@/lib/auth";2import { isAdmin } from "@/lib/admin";3import { prisma } from "@/lib/db";4import { NextResponse } from "next/server";5 6export async function GET() {7 const session = await auth();8 if (!session?.user?.email || !(await isAdmin(session.user.email))) {9 return NextResponse.json({ error: "Forbidden" }, { status: 403 });10 }11 12 const users = await prisma.user.findMany({13 include: {14 subscription: true,15 _count: {16 select: {17 todos: true, // Count todos per user18 accounts: true, // Count OAuth connections19 },20 },21 },22 orderBy: { createdAt: "desc" },23 });24 25 return NextResponse.json(users);26}Delete User Account
Thanks to onDelete: Cascade in the schema, deleting a user automatically removes all related data:
1import { auth } from "@/lib/auth";2import { prisma } from "@/lib/db";3import { NextResponse } from "next/server";4 5export async function DELETE() {6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });9 }10 11 // This will cascade delete:12 // - All OAuth accounts13 // - All sessions14 // - Subscription15 // - All todos16 await prisma.user.delete({17 where: { id: session.user.id },18 });19 20 return NextResponse.json({ success: true });21}Permanent Action
Session Management
How Sessions Work
The starter uses database sessions (not JWT):
- User logs in via OAuth provider
- Auth.js creates a session in the database
- Session token is stored in a cookie
- Each request validates the token against the database
- Sessions expire automatically based on the
expiresfield
View Active Sessions
1import { auth } from "@/lib/auth";2import { prisma } from "@/lib/db";3import { NextResponse } from "next/server";4 5export async function GET() {6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });9 }10 11 const sessions = await prisma.session.findMany({12 where: {13 userId: session.user.id,14 expires: {15 gte: new Date(), // Only non-expired sessions16 },17 },18 orderBy: { createdAt: "desc" },19 });20 21 return NextResponse.json(sessions);22}Revoke All Sessions
Force logout on all devices by deleting all sessions:
1import { auth, signOut } from "@/lib/auth";2import { prisma } from "@/lib/db";3import { NextResponse } from "next/server";4 5export async function POST() {6 const session = await auth();7 if (!session?.user) {8 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });9 }10 11 // Delete all sessions for this user12 await prisma.session.deleteMany({13 where: { userId: session.user.id },14 });15 16 // Sign out current session17 await signOut();18 19 return NextResponse.json({ success: true });20}User Subscription Access
Check User Plan
1import { prisma } from "@/lib/db";2 3export async function getUserPlan(userId: string) {4 const subscription = await prisma.subscription.findUnique({5 where: { userId },6 });7 8 return subscription?.planId || "free";9}10 11export async function isUserPro(userId: string) {12 const planId = await getUserPlan(userId);13 return planId === "pro" || planId === "enterprise";14}15 16export async function isUserEnterprise(userId: string) {17 const planId = await getUserPlan(userId);18 return planId === "enterprise";19}Restrict Features by Plan
1import { auth } from "@/lib/auth";2import { getUserPlan } from "@/lib/user";3import { redirect } from "next/navigation";4 5export default async function AnalyticsPage() {6 const session = await auth();7 if (!session?.user) {8 redirect("/login");9 }10 11 const plan = await getUserPlan(session.user.id);12 13 // Restrict to Pro and Enterprise only14 if (plan === "free") {15 return (16 <div>17 <h1>Analytics (Pro Feature)</h1>18 <p>Upgrade to Pro to access advanced analytics.</p>19 <a href="/pricing">View Plans</a>20 </div>21 );22 }23 24 return (25 <div>26 <h1>Advanced Analytics</h1>27 {/* Analytics dashboard */}28 </div>29 );30}User Statistics
Count Total Users
1import { prisma } from "@/lib/db";2 3export async function getTotalUsers() {4 return await prisma.user.count();5}6 7export async function getUsersByPlan() {8 const users = await prisma.subscription.groupBy({9 by: ["planId"],10 _count: true,11 });12 13 return users.reduce((acc, item) => {14 acc[item.planId] = item._count;15 return acc;16 }, {} as Record<string, number>);17}18 19export async function getNewUsersThisMonth() {20 const startOfMonth = new Date();21 startOfMonth.setDate(1);22 startOfMonth.setHours(0, 0, 0, 0);23 24 return await prisma.user.count({25 where: {26 createdAt: {27 gte: startOfMonth,28 },29 },30 });31}Authentication Helpers
Protected Route Wrapper
Create a reusable function to protect routes:
1import { auth } from "@/lib/auth";2import { redirect } from "next/navigation";3 4export async function requireAuth() {5 const session = await auth();6 7 if (!session?.user) {8 redirect("/login");9 }10 11 return session;12}13 14export async function requireAdmin() {15 const session = await requireAuth();16 17 if (!session.user.email || !(await isAdmin(session.user.email))) {18 redirect("/");19 }20 21 return session;22}Use in your pages:
1import { requireAuth } from "@/lib/auth-helpers";2 3export default async function DashboardPage() {4 const session = await requireAuth(); // Auto-redirects if not logged in5 6 return <div>Welcome, {session.user.name}!</div>;7}Sign In and Sign Out
1"use client";2 3import { signIn, signOut } from "next-auth/react";4 5export function SignInButton() {6 return (7 <button onClick={() => signIn()}>8 Sign In9 </button>10 );11}12 13export function SignOutButton() {14 return (15 <button onClick={() => signOut()}>16 Sign Out17 </button>18 );19}Troubleshooting
Session Always Null
- Verify database is running and accessible
- Check
DATABASE_URLis set in.env.local - Run
npm run db:devto ensure migrations are applied - Clear browser cookies and try logging in again
User Not Found After Login
- Check
Usertable in Prisma Studio:npm run db:studio - Verify OAuth provider created user record
- Check Auth.js logs for errors during account creation
Can't Delete User
- Ensure all relations have
onDelete: Cascadein schema - Check for foreign key constraints in the database
- Manually delete related records first if cascade isn't working
Best Practices
1. Always Check Authentication
Never assume a user is logged in. Always verify session before accessing user data or performing operations.
2. Use Server Components for Auth Checks
Prefer Server Components for authentication checks to avoid client-side auth bypass vulnerabilities.
3. Validate User Ownership
When users access their data, verify they own it:
1// Good: Verify user owns the todo2const todo = await prisma.todo.findFirst({3 where: {4 id: todoId,5 createdById: session.user.id, // Ownership check6 },7});8 9if (!todo) {10 return NextResponse.json({ error: "Not found" }, { status: 404 });11}4. Log User Actions
For audit purposes, log important user actions (account deletion, plan changes, etc.).
5. Handle Session Expiration
Provide a good UX when sessions expire by automatically redirecting to login or showing a clear message.
Related Guides
Next Steps
Now that you understand user management, you might want to:
- Learn about Admin Dashboard for managing users as an admin
- Explore Billing Setup to understand subscription management
- Review OAuth Providers for authentication configuration