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
Userparameter 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
userparameters 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
-
Before optimization, measure page load time:
- Dashboard with auth anti-pattern: ~1000ms
- Individual component renders: ~200-300ms each
-
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
userbut notdbparameter - 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 (inlib/actions/utils.ts) handles dynamic imports ofnext/headersautomatically - Cache TTL is 15 minutes (configurable in Better Auth settings)
- Session expiry is 7 days (stateless sessions - no database updates)
- Type safety: Use
EnhancedDBtype export from database package - Discriminated unions ensure perfect type narrowing (no optional chaining needed)
Common Mistakes
- Passing only
userto child components: Always pass bothuseranddb - Creating new context in loops: Reuse the same client for batch operations
- Ignoring acceptable patterns: Don't "fix" public endpoints or cache boundaries
- 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 helperauthApi(headers)- API route auth helper
References
- Better Auth v1.4.10 Documentation
- ZenStack ORM Documentation
- Next.js 16 Server Components
- Better Auth JWE Cookie Cache: 15-minute TTL, A256CBC-HS512 encryption
- Performance optimization guide in CLAUDE.md (Authentication section)
Weekly Installs
1
Repository
hankanman/claude-configFirst Seen
Mar 4, 2026
Security Audits
Installed on
windsurf1
amp1
cline1
openclaw1
trae1
qoder1