| name | nextjs-zenstack-auth-context-error |
| description | Fix for "An error occurred in the Server Components render" in Next.js production builds
when using ZenStack ORM with Better Auth. Use when: (1) Generic Server Components error
in production with no stack trace, (2) Works in dev but fails in production, (3) Using
manual db.$setAuth() instead of authenticated client from auth()/requireRole(). Symptoms
include Sentry errors with digest but no details, or blank error pages. Solution: Always
use the pre-configured db client from auth() or requireRole() result instead of manually
creating auth context with db.$setAuth({ id, role }).
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-07T00:00:00.000Z" |
Next.js + ZenStack Auth Context Error Fix
Problem
In Next.js production builds, manually creating ZenStack auth contexts with db.$setAuth()
can cause type mismatches that manifest as generic Server Components render errors. The
actual error details are hidden in production for security, making debugging extremely
difficult.
Context / Trigger Conditions
When this occurs:
- Next.js app in production shows generic error page
- Sentry/error tracking shows: "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details."
- Same code works perfectly in development environment
- Using ZenStack ORM with Better Auth
- Using Server Components or Server Actions with database queries
Specific code pattern that triggers this:
const authResult = await auth();
const { user } = authResult;
const userDB = db.$setAuth({ id: user.id, role: user.role });
Error symptoms:
- Production: Generic error page with no details
- Sentry: Error with digest ID but no stack trace
- Browser console: Empty (errors are server-side)
- Works in dev, fails in production
Root Cause
Type Mismatch in Production:
- Better Auth returns user with
role: string | null
- ZenStack expects
role: RoleEnum | null | undefined
- In development, TypeScript errors are warnings
- In production builds, type mismatches cause runtime errors
- Next.js hides error details for security
Why manual $setAuth() is problematic:
- Requires explicit type casting that's easy to forget
- Creates redundant auth contexts
- Bypasses the properly-typed auth helpers
- Makes code less maintainable
Solution
Always use the pre-configured database client from auth helpers:
Pattern 1: Server Components (Use auth())
import { auth } from "@/lib/auth";
export default async function Page() {
const authResult = await auth();
if (!authResult.authenticated) {
return redirect('/sign-in');
}
const { user, db: userDB } = authResult;
const data = await userDB.profile.findMany();
}
Pattern 2: Protected Routes (Use requireRole())
import { requireRole } from "@/lib/auth";
import { RoleEnum } from "database/types";
export default async function InstructorPage() {
const result = await requireRole([RoleEnum.INSTRUCTOR, RoleEnum.ADMIN]);
if ("redirect" in result) {
return redirect({ href: result.redirect, locale });
}
const { user, db: userDB } = result;
const profile = await userDB.profile.findFirst({
where: { userId: user.id }
});
}
Pattern 3: Server Actions (Use getAuthContext())
"use server";
import { getAuthContext } from "./utils";
export async function updateProfile(data: ProfileData) {
const auth = await getAuthContext();
if (!auth.authenticated) {
return auth;
}
const { user, db } = auth;
await db.profile.update({
where: { userId: user.id },
data
});
}
Anti-Pattern to Avoid
import { db } from "database";
const authResult = await auth();
const { user } = authResult;
const userDB = db.$setAuth({ id: user.id, role: user.role });
const userDB = db.$setAuth({
...user,
role: user.role as RoleEnum | null | undefined
});
Verification
After applying the fix:
-
Local verification:
pnpm lint
pnpm types
-
Production test:
- Deploy to production/staging
- Navigate to the previously failing route
- Verify page loads without errors
- Check Sentry for no new occurrences
-
Code review checklist:
grep -r "db\.\$setAuth({ id: user.id, role: user.role })" --include="*.ts" --include="*.tsx"
Example: Real-World Fix
Before (Broken in Production):
import { db } from "database";
export default async function CalendarPage() {
const result = await requireRole([RoleEnum.INSTRUCTOR]);
const { user } = result;
const userDB = db.$setAuth({ id: user.id, role: user.role });
const profile = await userDB.profile.findFirst({
where: { userId: user.id }
});
}
After (Works in Production):
export default async function CalendarPage() {
const result = await requireRole([RoleEnum.INSTRUCTOR]);
const { user, db: userDB } = result;
const profile = await userDB.profile.findFirst({
where: { userId: user.id }
});
}
Notes
Why This Happens
-
Next.js Security: Production builds strip error details to avoid leaking sensitive
information (Next.js Error Handling docs)
-
ZenStack Auth Context: ZenStack requires properly typed auth context for row-level
security policies (ZenStack Better-Auth Integration)
-
Type Safety: Better Auth uses discriminated unions for perfect type safety, but
manual auth context creation bypasses these guarantees
Related Issues
- If you see "Cannot read properties of undefined" in production but not dev, this could
be the same root cause
- If auth-related queries work in some pages but not others, check for inconsistent auth
context patterns
Prevention Strategy
Add to your project's CLAUDE.md or contributing guidelines:
## Database Client Usage
**ALWAYS** use the authenticated db client from auth helpers:
✅ Server Components: `const { db } = await auth()`
✅ Protected Routes: `const { db } = await requireRole([...])`
✅ Server Actions: `const { db } = await getAuthContext()`
❌ NEVER manually create auth context: `db.$setAuth({ id, role })`
Add to your linting/pre-commit hooks:
if grep -r "db\.\$setAuth({ id: user.id, role: user.role })" \
--include="*.ts" --include="*.tsx" apps/; then
echo "Error: Found manual auth context creation. Use auth() or requireRole() instead."
exit 1
fi
References