skills/hankanman/claude-config/better-auth-context-performance

better-auth-context-performance

Installation
SKILL.md

Better Auth Context Performance Anti-Pattern

Problem

When using Better Auth v1.4.10+ with ZenStack ORM, manually creating auth contexts with db.$setAuth() bypasses the 15-minute JWE cookie cache, causing unnecessary database queries and 5x slower performance.

Context / Trigger Conditions

This anti-pattern appears when:

  • Server actions or components call db.$setAuth({ ...user, role: ... }) directly
  • Functions accept a User parameter only to create an auth context
  • Dashboard loads slowly (>500ms when should be <200ms)
  • Multiple components in same request create separate auth contexts
  • You see patterns like: const userDB = db.$setAuth(user)

Symptoms:

  • Slow page loads (1000ms+ when should be 200-400ms)
  • Unnecessary database queries for auth verification
  • Redundant user parameters passed through component trees
  • Loss of cached session benefits

Solution

Pattern 1: Server Actions (Preferred)

Before (Anti-pattern):

"use server";

import { db } from "database";

export async function myAction(user: User, data: MyData) {
  if (!user?.id) return { success: false, error: "Unauthorized" };

  const userDB = db.$setAuth({  // ❌ Creating new context
    ...user,
    role: user.role as RoleEnum | null | undefined,
  });

  return await userDB.myModel.create({ data });
}

After (Optimized):

"use server";

import { getAuthContext } from "./utils";

export async function myAction(data: MyData) {
  const auth = await getAuthContext();  // ✅ Reuse cached client
  if (!auth.authenticated) return auth;

  const { user, db: userDB } = auth;
  return await userDB.myModel.create({ data });
}

Pattern 2: Server Components

Before (Anti-pattern):

import { db } from "database";
import { auth } from "@/lib/auth";

export async function MyComponent() {
  const authResult = await auth();
  if (!authResult.authenticated) return redirect('/sign-in');

  const { user } = authResult;

  // ❌ Creating new context instead of using cached one
  const userDB = db.$setAuth({
    ...user,
    role: user.role as RoleEnum | null | undefined,
  });

  const data = await userDB.myModel.findMany();
}

After (Optimized):

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

export async function MyComponent() {
  const authResult = await auth();
  if (!authResult.authenticated) return redirect('/sign-in');

  const { user, db: userDB } = authResult;  // ✅ Use provided client
  const data = await userDB.myModel.findMany();
}

Pattern 3: Pass DB Client to Child Components

Before (Anti-pattern):

// Parent
export default async function Page() {
  const authResult = await auth();
  if (!authResult.authenticated) return redirect('/sign-in');

  const { user } = authResult;
  return <MyComponent user={user} />;  // ❌ Only passing user
}

// Child
export async function MyComponent({ user }: { user: User }) {
  const userDB = db.$setAuth(user);  // ❌ Creating new context
  const data = await userDB.myModel.findMany();
}

After (Optimized):

// Parent
import type { EnhancedDB } from "database";

export default async function Page() {
  const authResult = await auth();
  if (!authResult.authenticated) return redirect('/sign-in');

  const { user, db: userDB } = authResult;
  return <MyComponent user={user} db={userDB} />;  // ✅ Pass both
}

// Child
import type { EnhancedDB } from "database";

interface MyComponentProps {
  user: User;
  db: EnhancedDB;  // ✅ Accept db parameter
}

export async function MyComponent({ user, db: userDB }: MyComponentProps) {
  const data = await userDB.myModel.findMany();  // ✅ Use provided client
}

Pattern 4: Optional DB Parameter for Flexibility

Before (Anti-pattern):

export async function getProfile(user: User) {
  const userDB = db.$setAuth(user);  // ❌ Always creates new context
  return await userDB.profile.findUnique({ where: { userId: user.id } });
}

After (Optimized):

import type { EnhancedDB } from "database";

export async function getProfile(user: User, userDB?: EnhancedDB) {
  // ✅ Reuse provided client if available, fallback to creating new one
  const dbClient = userDB || db.$setAuth({
    ...user,
    role: user.role as RoleEnum | null | undefined,
  });

  return await dbClient.profile.findUnique({ where: { userId: user.id } });
}

Verification

Performance Testing

  1. Before optimization, measure page load time:

    • Dashboard with auth anti-pattern: ~1000ms
    • Individual component renders: ~200-300ms each
  2. After optimization, measure again:

    • Dashboard with cached client: ~200-400ms
    • Individual component renders: ~50-100ms each
    • Expected improvement: 5x faster

Code Audit

Search codebase for anti-pattern instances:

# Find manual $setAuth calls
rg '\.\$setAuth\(' --type ts

# Look for User parameters used only for auth
rg 'function.*\(user: User' --type ts

Acceptable patterns (don't fix these):

  • Public endpoints: db.$setAuth(undefined) for unauthenticated access
  • "use cache" components: Cache boundaries need explicit context
  • Metadata generation: Must work for both auth/unauth users

Problem patterns (fix these):

  • Server actions creating new context when getAuthContext() available
  • Components accepting user but not db parameter
  • Nested components creating multiple contexts in same request

Performance Impact

Scenario Before After Improvement
Dashboard page 1000ms 200-400ms 5x faster
Vehicle CRUD 250ms 50ms 5x faster
Profile load 300ms 60ms 5x faster
Search action 500ms 400ms 1.25x faster

Why 5x improvement?

  • Better Auth JWE cookie cache: 15-minute TTL, 99%+ hit rate
  • Manual $setAuth(): Bypasses cache, creates new context overhead
  • Reusing cached client: Zero auth overhead, direct database access

Notes

  • This optimization applies specifically to Better Auth v1.4.10+ with JWE encrypted cookie cache
  • ZenStack ORM 3.1.0+ with enhanced database client required
  • The getAuthContext() helper (in lib/actions/utils.ts) handles dynamic imports of next/headers automatically
  • Cache TTL is 15 minutes (configurable in Better Auth settings)
  • Session expiry is 7 days (stateless sessions - no database updates)
  • Type safety: Use EnhancedDB type export from database package
  • Discriminated unions ensure perfect type narrowing (no optional chaining needed)

Common Mistakes

  1. Passing only user to child components: Always pass both user and db
  2. Creating new context in loops: Reuse the same client for batch operations
  3. Ignoring acceptable patterns: Don't "fix" public endpoints or cache boundaries
  4. Forgetting type exports: Add export type EnhancedDB = typeof db; to database package

Related Patterns

  • getAuthContext() - Server action auth helper (dynamic import of next/headers)
  • auth() - Server component auth helper (returns both user and db)
  • requireRole() - Role-based access control helper
  • authApi(headers) - API route auth helper

References

Weekly Installs
1
First Seen
Mar 4, 2026
Installed on
windsurf1
amp1
cline1
openclaw1
trae1
qoder1