StarterApp Docs
Packages

@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

Session structure
{
  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.

Header wrapper usage
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

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().

app/providers.tsx
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:

lib/auth/server.ts
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:

app/api/autumn/[...all]/route.ts
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:

Type utilities
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.