@workspace/auth
BetterAuth bridges for Next.js and Convex authentication
Unified authentication across Next.js and Convex surfaces. This package bridges BetterAuth to server components, API routes, and client hooks, keeping session state synchronized everywhere.
Package Exports
Server-Side Authentication
auth.api.getSession()
Server-side session retrieval compatible with UseAutumn integration.
Prop
Type
import { headers } from "next/headers";
import { auth } from "@workspace/auth/server";
export async function GET() {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session?.user?.id) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return Response.json({ userId: session.user.id });
}
Heads up
The headers
parameter exists for UseAutumn compatibility. Internally, authentication reads from Next.js context via getToken(createAuth)
from @convex-dev/better-auth/nextjs
.
Return Type
{
user: {
id: string; // BetterAuth userId
name?: string;
email?: string;
image?: string;
emailVerified?: boolean;
};
session: {
id: string; // Convex document ID
userId: string;
expiresAt: Date;
};
} | null
getSessionFromRequestHeaders()
Convenience wrapper around auth.api.getSession()
for explicit header passing patterns.
import { getSessionFromRequestHeaders } from "@workspace/auth/server";
const session = await getSessionFromRequestHeaders(headers);
Client-Side Authentication
authClient
React client configured with Convex integration. Uses createAuthClient
from better-auth/react
with the convexClient
plugin.
"use client";
import { useSession } from "@workspace/auth/client";
export function ProfileCard() {
const { data: session, isPending } = useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Not signed in</div>;
return (
<div>
<h2>{session.user.name}</h2>
<p>{session.user.email}</p>
</div>
);
}
import { signIn } from "@workspace/auth/client";
function LoginButton() {
return (
<button onClick={() => signIn.social({ provider: "google" })}>
Sign in with Google
</button>
);
}
import { signOut } from "@workspace/auth/client";
function LogoutButton() {
return (
<button onClick={() => signOut()}>
Sign out
</button>
);
}
Type Export
import type { AuthClient } from "@workspace/auth/client";
The AuthClient
type represents the full authClient
instance for advanced usage patterns.
Architecture
Server Authentication Flow
Request arrives at Next.js API route or Server Component
auth.api.getSession()
calls getToken(createAuth)
to read session from Next.js context
Token is passed to Convex via fetchQuery(api.auth.getCurrentUser, {}, { token })
Convex returns user data, which is mapped to BetterAuth session format
Token-Based Pattern
The server bridge uses getToken
+ fetchQuery
instead of raw HTTP sessions. This pattern keeps authentication logic in Convex while maintaining Next.js compatibility.
Client Authentication Flow
The authClient
integrates with ConvexBetterAuthProvider
in app/providers.tsx
. Session state propagates through React Context, available to all client components via useSession()
.
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/client";
import { authClient } from "@workspace/auth/client";
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ConvexBetterAuthProvider client={authClient}>
{children}
</ConvexBetterAuthProvider>
);
}
Security Considerations
HttpOnly Cookies
Session tokens are stored in HttpOnly cookies, preventing JavaScript access and XSS-based token theft.
Secure Flag
Production deployments enforce Secure cookie flag, limiting transmission to HTTPS.
Rate Limiting
Apply rate limits to /api/auth/*
routes to prevent brute force attacks on authentication endpoints.
No-Store Cache
Authentication proxy route (/api/auth/[...all]
) uses no-store
to prevent credential caching.
Integration Patterns
With Convex Queries
Server-side code can bypass the auth bridge by using the pattern in ~/lib/auth/server
:
import { getToken } from "@convex-dev/better-auth/nextjs";
import { fetchQuery } from "convex/nextjs";
import { createAuth } from "@convex/lib/auth";
import { api } from "@convex/_generated/api";
export async function getCurrentSession() {
const token = await getToken(createAuth);
if (!token) return null;
return await fetchQuery(api.auth.getCurrentUser, {}, { token });
}
This direct pattern is preferred for application code. The auth.api.getSession()
bridge exists primarily for UseAutumn integration.
With UseAutumn Billing
Billing operations identify users via auth.api.getSession({ headers })
inside the Autumn route's identify function:
import { auth } from "@workspace/auth/server";
const handler = Autumn.nextHandler({
identify: async (req) => {
const session = await auth.api.getSession({
headers: req.headers
});
return session?.user?.id ?? null;
},
});
Protected Route Pattern
import { headers } from "next/headers";
import { auth } from "@workspace/auth/server";
import { secureUserJson, secureErrorJson } from "@workspace/security";
export async function PUT(request: Request) {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session?.user?.id) {
return secureErrorJson(
{ message: "Unauthorized", code: "UNAUTHORIZED" },
{ status: 401 }
);
}
const data = await request.json();
await updateUserSettings(session.user.id, data);
return secureUserJson({ success: true });
}
import { headers } from "next/headers";
import { auth } from "@workspace/auth/server";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session?.user?.id) {
redirect("/login");
}
return <div>Welcome, {session.user.name}!</div>;
}
"use client";
import { useSession, signOut } from "@workspace/auth/client";
export function UserMenu() {
const { data: session, isPending } = useSession();
if (isPending) {
return <div>Loading...</div>;
}
if (!session) {
return <a href="/login">Sign in</a>;
}
return (
<div>
<span>{session.user.email}</span>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}
Type Safety
All exports are fully typed. Import types explicitly when needed:
import type { AuthClient } from "@workspace/auth/client";
// Session type from useSession hook
type SessionData = NonNullable<ReturnType<typeof useSession>["data"]>;
The auth package serves as a compatibility layer between BetterAuth's session model and StarterApp's Convex backend. For application code, prefer the direct getToken
+ fetchQuery
pattern shown in ~/lib/auth/server
.