StarterApp Docs
Authentication

AI Patterns

LLM-optimized authentication with BetterAuth

BetterAuth provides first-party LLM compatibility through its official llms.txt. Combined with StarterApp context engineering in llms/AUTH_PATTERNS.md, AI assistants implement authentication requirements correctly on first attempt.

Layered Context Engineering

The authentication system combines BetterAuth official documentation, codebase-specific patterns in llms/AUTH_PATTERNS.md, working templates in llms/templates/, and security requirements in llms/SECURITY.md. This layered approach enables accurate AI-assisted development.

Context Engineering Strategy

The authentication system uses layered context engineering to enable accurate AI-assisted development:

BetterAuth Official Context

BetterAuth maintains an official llms.txt at https://www.better-auth.com/llms.txt with:

  • Complete API reference
  • Provider configuration patterns
  • Session management details
  • Plugin usage examples

Codebase-Specific Patterns

llms/AUTH_PATTERNS.md documents implementation patterns specific to this stack:

# AUTH_PATTERNS.md — BetterAuth + Convex Quick Reference

**Stack**: BetterAuth → Convex → Next.js 15 (no HTTP to auth, use `getToken`)

## Core Rules

* **Server data**: `getToken(createAuth) → fetchQuery(...)` (never HTTP fetch)
* **Protected pages**: Export cache flags (see below)
* **Import paths**: `createAuth` from `convex/lib/auth` (never `convex/http.ts`)
* **Identity**: BetterAuth `userId` maps directly (no complex resolution)
* **Convex identity helpers**: Import via `convex/lib/identity` adapter
  * `getUserIdOrThrow` throws `AppError('UNAUTHENTICATED')` on failure
  * `assertOwnerByUserId(record, userId, { concealExistence })` hides existence with `NOT_FOUND`

Working Templates

Template files in llms/templates/ provide complete, copy-paste patterns:

  • auth-api-route-template.ts - Protected API endpoint with CSRF
  • auth-convex-functions-template.ts - Authenticated Convex functions
  • auth-page-template.tsx - Protected page component
  • auth-server-component-template.tsx - Server component patterns

Security Requirements

llms/SECURITY.md enforces security patterns that AI must follow:

  • CSRF protection for mutations
  • Secure response headers
  • Cache configuration for auth routes
  • Error handling without information leakage

AI Capabilities

With comprehensive context, AI assistants accurately implement:

OAuth Provider Setup

"Add Google OAuth to the authentication system"
"Configure GitHub provider for sign-in"
"Enable Microsoft Azure AD authentication"

The AI reads convex/lib/auth.ts to understand the computeSocialProviders function and adds providers correctly:

export function computeSocialProviders(env: Record<string, string | undefined>) {
  const id = env.GOOGLE_CLIENT_ID?.trim();
  const secret = env.GOOGLE_CLIENT_SECRET?.trim();

  if (id && secret) {
    return {
      google: {
        clientId: id,
        clientSecret: secret,
      },
    };
  }
  return;
}

Protected Route Implementation

"Protect the dashboard layout with authentication"
"Add authentication to the billing page"
"Require sign-in for all routes under /app"

The AI references apps/dashboard/app/(dashboard)/layout.tsx and implements the pattern:

export const revalidate = 0;
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";

export default async function ProtectedLayout({ children }: PropsWithChildren) {
  const session = await getCurrentSession();
  if (!session) redirect("/sign-in");
  // Layout implementation
}

API Route Protection

"Create a protected API endpoint for user settings"
"Add authentication and CSRF protection to POST /api/support"
"Implement authenticated webhook handler"

The AI follows llms/templates/auth-api-route-template.ts:

export const runtime = "nodejs";
export const revalidate = 0;
export const dynamic = "force-dynamic";

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 }
      );
    }
    // Implementation
  } catch (error) {
    return secureErrorJson(
      { message: "Internal server error", code: "Internal" },
      { status: 500, userScoped: true }
    );
  }
}

Convex Function Protection

"Protect the support ticket query with user authentication"
"Add ownership validation to the document mutation"
"Create authenticated action for premium exports"

The AI uses patterns from llms/templates/auth-convex-functions-template.ts:

import { getUserIdOrThrow, assertOwnerByUserId } from "./lib/identity";

export const getPrivateNote = query({
  args: { noteId: v.id("notes") },
  handler: async (ctx, { noteId }) => {
    const userId = await getUserIdOrThrow(ctx);
    const note = await ctx.db.get(noteId);
    assertOwnerByUserId(note, userId, { concealExistence: true });
    return note;
  },
});

Context File Details

Implementation Flow

When an AI assistant receives an authentication task, it follows this flow:

Read Context Files

  1. Parse llms/AUTH_PATTERNS.md for patterns
  2. Review relevant template files
  3. Check llms/SECURITY.md for requirements

Identify Pattern

Match the request to the correct pattern:

  • Server component → requireUser() or getCurrentSession()
  • API route → auth.api.getSession() with CSRF checks
  • Convex function → getUserIdOrThrow() with identity helpers
  • Client component → useSession() or useQuery(api.auth.getCurrentUser)

Apply Template

Copy the appropriate template and adapt to the specific requirement:

  • Update types and schemas
  • Adjust error messages
  • Customize business logic
  • Maintain security patterns

Validate Implementation

Ensure the implementation includes:

  • Required cache exports for protected routes
  • CSRF protection for mutations
  • Proper error handling with secureErrorJson
  • Type safety with Zod validation
  • Security headers on responses

Testing AI Understanding

Verify AI comprehension with these prompts:

Session Access:

"Get the current user session in a server component"
"Add a sign-out button to the navigation"
"Show user profile in the dashboard sidebar"

Expected: Correct import paths, proper null handling, loading states

OAuth Configuration:

"Add GitHub OAuth provider to authentication"
"Configure Microsoft Azure AD for enterprise sign-in"
"Enable magic link authentication"

Expected: Environment variable setup, computeSocialProviders modification, BetterAuth configuration

Protected Endpoints:

"Create authenticated API endpoint for support tickets"
"Protect the admin dashboard layout"
"Add ownership validation to document queries"

Expected: CSRF protection, cache exports, identity helpers, secure error responses

Common AI Implementation Patterns

Pattern 1: Protected Server Component

Prompt: "Protect the settings page with authentication"

Expected Implementation:

import { requireUser } from "~/lib/auth/server";

export const revalidate = 0;
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";

export default async function SettingsPage() {
  const user = await requireUser();

  return (
    <div>
      <h1>Settings for {user.name}</h1>
      {/* Page content */}
    </div>
  );
}

Pattern 2: Protected API Route with CSRF

Prompt: "Create POST /api/tickets with authentication and CSRF protection"

Expected Implementation:

import { auth } from "@workspace/auth/server";
import { secureErrorJson, secureUserJson } from "@workspace/security";
import { headers } from "next/headers";
import { z } from "zod";
import { assertOrigin, assertFetchMetadata } from "~/lib/security/csrf";

export const runtime = "nodejs";
export const revalidate = 0;
export const dynamic = "force-dynamic";

const BodySchema = z.object({
  title: z.string().trim().min(1).max(200),
  description: z.string().trim().min(10).max(5000),
});

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 }
      );
    }

    const body = BodySchema.parse(await req.json());
    // Create ticket logic

    return secureUserJson({ ok: true, ticket }, { status: 201 });
  } catch (error) {
    if (error instanceof z.ZodError) {
      return secureErrorJson(
        { message: "Validation failed", code: "ValidationError" },
        { status: 400, userScoped: true }
      );
    }
    return secureErrorJson(
      { message: "Internal server error", code: "Internal" },
      { status: 500, userScoped: true }
    );
  }
}

Pattern 3: Protected Convex Query

Prompt: "Create authenticated query for user documents"

Expected Implementation:

import { query } from "./_generated/server";
import { v } from "convex/values";
import { getUserIdOrThrow } from "./lib/identity";

export const listMyDocuments = query({
  args: { limit: v.number() },
  handler: async (ctx, { limit }) => {
    const userId = await getUserIdOrThrow(ctx);

    const docs = await ctx.db
      .query("documents")
      .withIndex("by_user_id", (q) => q.eq("userId", userId))
      .take(Math.min(limit, 50));

    return docs;
  },
});

Context File Locations

Prop

Type