Sessions
Session management with BetterAuth
BetterAuth manages user sessions through secure HTTP-only cookies. Sessions last 7 days and refresh automatically during active use.
Session Architecture
Session data flows through three contexts: server components use getCurrentSession(), client hooks use useSession(), and API routes use auth.api.getSession(). Each context provides type-safe session data appropriate for its environment.
Session Structure
The authentication system provides session data in different shapes depending on the context:
Server components receive a UserSession
type from getCurrentSession()
:
export type UserSession = {
user: {
id: string; // BetterAuth user ID
name: string;
email: string;
image?: string;
};
session: {
userDocId: string; // Convex user document ID
userId: string; // BetterAuth user ID
};
};
The session includes both BetterAuth metadata and Convex document identifiers for data queries.
The useSession
hook from @workspace/auth/client
provides reactive session state:
type SessionData = {
user: {
id: string;
name: string;
email: string;
image?: string;
};
session: {
id: string;
userId: string;
expiresAt: Date;
};
};
API routes using auth.api.getSession()
receive the same structure as client hooks, compatible with UseAutumn billing integration.
Server-Side Patterns
Server components and route handlers use packages/app-shell/src/lib/auth/server.ts
:
Protected Server Component
import { requireUser } from "~/lib/auth/server";
export default async function DashboardPage() {
const user = await requireUser(); // Redirects to /sign-in if unauthenticated
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Email: {user.email}</p>
</div>
);
}
Optional Authentication
import { getCurrentSession } from "~/lib/auth/server";
export default async function HomePage() {
const session = await getCurrentSession(); // Returns null if unauthenticated
if (session) {
return <AuthenticatedView user={session.user} />;
}
return <PublicView />;
}
Protected Layout
The dashboard layout in apps/dashboard/app/(dashboard)/layout.tsx
protects all nested routes:
export const revalidate = 0;
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";
export default async function DashboardRootLayout({ children }: PropsWithChildren) {
const session = await getCurrentSession();
if (!session) {
redirect("/sign-in");
}
return (
<div className="flex min-h-screen">
<Sidebar user={session.user} />
<main>{children}</main>
</div>
);
}
Cache Configuration
Protected routes must export revalidate = 0
, dynamic = 'force-dynamic'
, and fetchCache = 'force-no-store'
to prevent session data from being cached.
Client-Side Patterns
Client components use React hooks from @workspace/auth/client
:
Basic Session Hook
"use client";
import { useSession } from "@workspace/auth/client";
export function UserProfile() {
const { data: session, isPending } = useSession();
if (isPending) {
return <div>Loading...</div>;
}
if (!session?.user) {
return <div>Please sign in</div>;
}
return (
<div>
<img src={session.user.image} alt={session.user.name} />
<p>{session.user.name}</p>
<p>{session.user.email}</p>
</div>
);
}
Convex Real-Time Auth
For real-time authentication state, use the Convex getCurrentUser
query:
"use client";
import { useQuery } from "convex/react";
import { api } from "@convex/_generated/api";
export function LiveUserProfile() {
const user = useQuery(api.auth.getCurrentUser);
if (user === undefined) {
return <div>Loading...</div>;
}
if (user === null) {
return <div>Please sign in</div>;
}
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Member since {new Date(user.createdAt).toLocaleDateString()}</p>
</div>
);
}
The Convex query returns the combined user object from convex/auth.ts
:
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
const userMetadata = await betterAuthComponent.getAuthUser(ctx);
if (!userMetadata) return null;
const user = await ctx.db.get(userMetadata.userId);
if (!user) return null;
return { ...user, ...userMetadata }; // Merged data
},
});
API Route Patterns
API routes use auth.api.getSession()
from @workspace/auth/server
:
Protected API Endpoint
import { auth } from "@workspace/auth/server";
import { headers } from "next/headers";
import type { NextRequest } from "next/server";
export const runtime = "nodejs";
export const revalidate = 0;
export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) {
return new Response("Unauthorized", { status: 401 });
}
// Proceed with authenticated logic
return Response.json({
userId: session.user.id,
name: session.user.name,
});
}
With CSRF Protection
For mutation endpoints, combine session checks with CSRF validation:
import { auth } from "@workspace/auth/server";
import { secureErrorJson, secureUserJson } from "@workspace/security";
import { headers } from "next/headers";
import { assertOrigin, assertFetchMetadata } from "~/lib/security/csrf";
export async function POST(req: NextRequest) {
try {
assertOrigin(req);
assertFetchMetadata(req);
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user?.id) {
return secureErrorJson(
{ message: "Authentication required", code: "Unauthorized" },
{ status: 401, userScoped: true }
);
}
// Mutation logic here
return secureUserJson({ ok: true });
} catch (error) {
return secureErrorJson(
{ message: "Request failed", code: "Internal" },
{ status: 500, userScoped: true }
);
}
}
Security Pattern
Use secureUserJson
and secureErrorJson
from @workspace/security
for all API responses. These helpers add security headers and prevent information leakage.
Session Lifecycle
Session Creation
Sessions are created when users authenticate via OAuth or credentials through the /api/auth/*
endpoints.
Cookie Storage
BetterAuth stores the session token in an HTTP-only cookie:
- Development:
session
cookie - Production:
__Host-session
cookie with Secure flag
Automatic Refresh
Sessions refresh automatically when accessed after the updateAge
threshold (24 hours):
session: {
expiresIn: 60 * 60 * 24 * 7, // 7 days
updateAge: 60 * 60 * 24, // Refresh after 24 hours
}
Session Expiration
Sessions expire after 7 days of inactivity. Expired sessions automatically redirect to /sign-in
.
Implementation Details
Token Flow
Server components use the getToken
pattern from @convex-dev/better-auth/nextjs
:
export async function getCurrentSession(): Promise<UserSession | null> {
const createAuth = await getCreateAuthFunction();
const token = await getToken(createAuth); // Reads BetterAuth cookie
if (!token) return null;
const user = await fetchQuery(api.auth.getCurrentUser, {}, { token });
if (!user) return null;
return {
user: {
id: user.userId,
name: user.name,
email: user.email,
image: user.image,
},
session: {
userDocId: user._id,
userId: user.userId,
},
};
}
This pattern:
- Reads the BetterAuth session cookie
- Generates a Convex authentication token
- Queries Convex for user data
- Returns a typed session object
Never Use HTTP Fetch
Always use getToken(createAuth)
followed by fetchQuery
. Never make HTTP requests to BetterAuth endpoints from server components.
Provider Setup
Client components require the ConvexBetterAuthProvider
wrapper in apps/dashboard/app/providers.tsx
:
"use client";
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
import { authClient } from "@workspace/auth/client";
import { convexClient } from "@workspace/convex/client";
export function Providers({ children }: React.PropsWithChildren) {
return (
<ConvexBetterAuthProvider authClient={authClient} client={convexClient}>
{children}
</ConvexBetterAuthProvider>
);
}
This provider enables useSession()
and Convex authentication hooks throughout the client component tree.