skills/hankanman/claude-config/nextjs-zenstack-auth-context-error

nextjs-zenstack-auth-context-error

Installation
SKILL.md

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:

// ❌ ANTI-PATTERN - Causes production errors
const authResult = await auth();
const { user } = authResult;

// Manually creating auth context
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:

  1. Better Auth returns user with role: string | null
  2. ZenStack expects role: RoleEnum | null | undefined
  3. In development, TypeScript errors are warnings
  4. In production builds, type mismatches cause runtime errors
  5. 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())

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

export default async function Page() {
  const authResult = await auth();

  if (!authResult.authenticated) {
    return redirect('/sign-in');
  }

  // Use db from authResult - already configured with correct types
  const { user, db: userDB } = authResult;

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

Pattern 2: Protected Routes (Use requireRole())

// ✅ CORRECT
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 });
  }

  // Use db from result - already configured with correct types
  const { user, db: userDB } = result;

  const profile = await userDB.profile.findFirst({
    where: { userId: user.id }
  });
}

Pattern 3: Server Actions (Use getAuthContext())

// ✅ CORRECT
"use server";

import { getAuthContext } from "./utils";

export async function updateProfile(data: ProfileData) {
  const auth = await getAuthContext();

  if (!auth.authenticated) {
    return auth; // Returns error response
  }

  // Use db from auth - already configured with correct types
  const { user, db } = auth;

  await db.profile.update({
    where: { userId: user.id },
    data
  });
}

Anti-Pattern to Avoid

// ❌ WRONG - Causes production errors
import { db } from "database";

const authResult = await auth();
const { user } = authResult;

// DON'T manually create auth context
const userDB = db.$setAuth({ id: user.id, role: user.role });

// Even with type casting, this is an anti-pattern:
const userDB = db.$setAuth({
  ...user,
  role: user.role as RoleEnum | null | undefined
});

Verification

After applying the fix:

  1. Local verification:

    pnpm lint
    pnpm types
    
  2. Production test:

    • Deploy to production/staging
    • Navigate to the previously failing route
    • Verify page loads without errors
    • Check Sentry for no new occurrences
  3. Code review checklist:

    # Search for the anti-pattern in your codebase
    grep -r "db\.\$setAuth({ id: user.id, role: user.role })" --include="*.ts" --include="*.tsx"
    
    # Should return 0 results
    

Example: Real-World Fix

Before (Broken in Production):

// apps/web/app/[locale]/(app)/settings/calendar/page.tsx
import { db } from "database";

export default async function CalendarPage() {
  const result = await requireRole([RoleEnum.INSTRUCTOR]);
  const { user } = result;

  // ❌ Creating new auth context manually
  const userDB = db.$setAuth({ id: user.id, role: user.role });

  const profile = await userDB.profile.findFirst({
    where: { userId: user.id }
  });
}

After (Works in Production):

// apps/web/app/[locale]/(app)/settings/calendar/page.tsx
export default async function CalendarPage() {
  const result = await requireRole([RoleEnum.INSTRUCTOR]);

  // ✅ Use db from requireRole result
  const { user, db: userDB } = result;

  const profile = await userDB.profile.findFirst({
    where: { userId: user.id }
  });
}

Notes

Why This Happens

  1. Next.js Security: Production builds strip error details to avoid leaking sensitive information (Next.js Error Handling docs)

  2. ZenStack Auth Context: ZenStack requires properly typed auth context for row-level security policies (ZenStack Better-Auth Integration)

  3. 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:

# Check for the anti-pattern
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

Weekly Installs
2
First Seen
Mar 4, 2026
Installed on
qoder2
gemini-cli2
claude-code2
github-copilot2
windsurf2
codex2