Documentation
Documentation
Introduction

Getting Started

Getting StartedInstallationQuick StartProject Structure

Architecture

Architecture OverviewTech StacktRPC MiddlewareDesign Principles

Patterns

Code Patterns & ConventionsFeature ModulesError HandlingType Safety

Database

DatabaseSchema DefinitionDatabase OperationsMigrationsCaching

API

tRPCProceduresRouterstRPC Proxy Setup
APIsOpenAPIREST Endpoints

Auth & Access

AuthenticationConfigurationOAuth ProvidersRolesSession Management
AuthorizationUser RolesPermissions

Routing & i18n

RoutingDeclarative RoutingNavigation
InternationalizationTranslationsLocale Routing

Components & UI

ComponentsButtonsFormsNavigationDialogs
StylesTailwind CSSThemingTypography

Storage

StorageConfigurationUsageBuckets

Configuration

ConfigurationEnvironment VariablesFeature Flags

Templates

Template GuidesCreate New FeatureCreate New PageCreate Database TableCreate tRPC RouterAdd Translations

Development

DevelopmentCommandsAI AgentsBest Practices

Session Management

Managing user sessions

Server-Side Session

All server-side auth utilities are available through the auth object.

Get Current Session

import { getAuth } from "@/lib/auth/server";

// Cached session retrieval (recommended)
const auth = await getAuth();

// auth: { user, session } | null
if (auth?.user) {
  console.log("Logged in as:", auth.user.email);
  console.log("Session expires:", auth.session.expiresAt);
  console.log("User role:", auth.user.role);
}

Direct API Access

For custom scenarios, use the direct API:

import { auth } from "@/lib/auth/server";
import { headers } from "next/headers";

const session = await auth.api.getSession({
  headers: await headers(),
});

Layout Guards

Use layout guards to protect routes at the layout level:

import { auth } from "@/lib/auth/server";
import { UserRole } from "@/db/enums";

// Require specific role(s)
const session = await auth.requireRole(UserRole.USER);
const session = await auth.requireRole([UserRole.USER, UserRole.ADMIN]);

// With callback preservation
await auth.requireRole(UserRole.ADMIN, { preserveCallback: true });

// With onboarding required
await auth.requireRole(UserRole.ADMIN, { requireOnboardingCompleted: true });

// Require unauthenticated (for login/register pages)
await auth.requireUnauthenticated();

In tRPC Procedures

// Session is automatically available in context
export const myRouter = createTRPCRouter({
  protectedRoute: authProcedure.query(async ({ ctx }) => {
    // ctx.user is guaranteed to be non-null
    const userId = ctx.user.id;
    const userEmail = ctx.user.email;
    return { userId, userEmail };
  }),
});

In API Routes

import { getAuth } from "@/lib/auth/server";
import { unauthorized } from "next/navigation";
import { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  const auth = await getAuth();

  if (!auth) {
    unauthorized();
  }

  // Proceed with authenticated request
  return Response.json({ user: auth.user });
}

Client-Side Session

useAuth Hook

The primary hook for accessing auth state on the client:

"use client";

import { useAuth } from "@/lib/auth/client";

export function UserProfile() {
  const { user, session, isAuthenticated, signOut } = useAuth();

  if (!isAuthenticated) return <LoginPrompt />;

  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Role: {user.role}</p>
      <p>Session expires: {session.expiresAt.toISOString()}</p>
      <button onClick={() => signOut()}>Logout</button>
    </div>
  );
}

useAuth Return Values

PropertyTypeDescription
userUser | nullCurrent user object
sessionSession | nullCurrent session
isAuthenticatedbooleanWhether user is logged in
signOutfunctionSign out function

Auth Actions

Sign In

import { authClient } from "@/lib/auth/client";

// Sign in with email/password
await authClient.signIn.email({
  email: "user@example.com",
  password: "password123",
});

// Sign in with OAuth
await authClient.signIn.social({
  provider: "google",
  callbackURL: PageDashboard(),
});

// Sign in with magic link
await authClient.signIn.magicLink({
  email: "user@example.com",
  callbackURL: PageDashboard(),
});

Sign Up

await authClient.signUp.email({
  email: "user@example.com",
  password: "password123",
  name: "John Doe",
});

Sign Out

await authClient.signOut();

Session Freshness

For sensitive operations, require a recent authentication:

// Configure in AuthConfig
sessionFreshAge: 300, // 5 minutes in seconds

// In procedure
export const sensitiveRouter = createTRPCRouter({
  deleteAccount: authProcedure.mutation(async ({ ctx }) => {
    if (ctx.session.fresh === false) {
      throw new TRPCError({
        code: "FORBIDDEN",
        message: "Please re-authenticate",
      });
    }
    
    // Proceed with sensitive operation
  }),
});

Session Storage Options

JWT (Default)

Stateless sessions stored in cookie:

// src/config/app.ts
export const AuthConfig = {
  storeSessionInDatabase: false, // Default
};

Advantages:

  • No database queries
  • Fast performance
  • Scales horizontally

Disadvantages:

  • Cannot revoke immediately
  • Larger cookie size

Database Sessions

Stateful sessions with revocation support:

// src/config/app.ts
export const AuthConfig = {
  storeSessionInDatabase: true,
};

Advantages:

  • Immediate session revocation
  • Smaller cookie size
  • Track active sessions

Disadvantages:

  • Database query per request
  • Requires database connection

Session Lifecycle

Session Creation

Automatically created on successful authentication:

  1. User logs in with credentials or OAuth
  2. Server validates credentials
  3. Session token generated
  4. Token stored in cookie
  5. User redirected to callback URL

Session Refresh

Sessions are automatically refreshed:

// Disable auto-refresh if needed
export const AuthConfig = {
  disableSessionRefresh: true,
};

Session Expiration

Sessions expire based on configuration:

// Check expiration
if (auth?.session.expiresAt < new Date()) {
  // Session expired - redirect to login
}

Session Revocation

Manual Sign Out

await authClient.signOut();

Revoke All Sessions (Database Mode)

await auth.api.revokeAllSessions({ userId: user.id });

Account Management

Change Password

import { FormChangePassword } from "@/features/auth/components";

<FormChangePassword />

Change Email

import { FormChangeEmail } from "@/features/auth/components";

<FormChangeEmail currentEmail={user.email} />

Delete Account

import { FormDeleteAccount } from "@/features/auth/components";

// Requires typing "DELETE" to confirm
<FormDeleteAccount />

Best Practices

1. Always Use Server-Side Checks

// ❌ Bad - Client-only check
if (user?.role === "admin") {
  // Can be bypassed
}

// ✅ Good - Server-side check
await auth.requireRole(UserRole.ADMIN);

2. Handle Auth Errors

try {
  const auth = await getAuth();
  if (!auth) throw new Error("Unauthorized");
} catch (error) {
  redirect(PageLogin());
}

3. Use Layout Guards

// app/[locale]/(admin)/layout.tsx
export default async function Layout({ children }) {
  await auth.requireRole(UserRole.ADMIN, { preserveCallback: true });
  return <AdminLayout>{children}</AdminLayout>;
}

4. Implement Session Freshness

For sensitive operations like account deletion:

if (ctx.session.fresh === false) {
  return {
    success: false,
    message: ctx.t("auth.reAuthRequired"),
  };
}

Next Steps

OAuth Providers

User Roles

Configuration

On this page

Server-Side Session
Get Current Session
Direct API Access
Layout Guards
In tRPC Procedures
In API Routes
Client-Side Session
useAuth Hook
useAuth Return Values
Auth Actions
Sign In
Sign Up
Sign Out
Session Freshness
Session Storage Options
JWT (Default)
Database Sessions
Session Lifecycle
Session Creation
Session Refresh
Session Expiration
Session Revocation
Manual Sign Out
Revoke All Sessions (Database Mode)
Account Management
Change Password
Change Email
Delete Account
Best Practices
1. Always Use Server-Side Checks
2. Handle Auth Errors
3. Use Layout Guards
4. Implement Session Freshness
Next Steps