zenstack-authenticated-db-params
Installation
SKILL.md
ZenStack Authenticated DB Parameter Pattern
Problem
When creating utility functions that need to accept an authenticated ZenStack database client
(to enforce access control policies), TypeScript throws circular reference errors if you
try to use ReturnType<typeof db.$setAuth> directly in the function signature.
Context / Trigger Conditions
- TypeScript Error:
TS2502: 'db' is referenced directly or indirectly in its own type annotation - Project Stack: ZenStack ORM (built on Prisma) + Better Auth
- Scenario: Refactoring utilities to accept authenticated
dbinstead of usingbaseDBdirectly - Pattern: Server actions call utilities and need to pass their authenticated
dbclient
Solution
Step 1: Import db Client and Create Type Alias
Instead of importing EnhancedPrismaClient (which doesn't exist), import the base db client
and create a local type alias:
// ❌ WRONG - EnhancedPrismaClient doesn't exist
import type { EnhancedPrismaClient } from "database";
export async function myUtil(userId: string, db: EnhancedPrismaClient) {
// ...
}
// ✅ CORRECT - Import db client and create local type
import { db as dbClient } from "database";
type AuthenticatedDB = ReturnType<typeof dbClient.$setAuth>;
export async function myUtil(userId: string, db: AuthenticatedDB) {
// ...
}
Step 2: Update Utility Function Signatures
Use the AuthenticatedDB type for all database parameters:
"use server";
import { db as dbClient } from "database";
type AuthenticatedDB = ReturnType<typeof dbClient.$setAuth>;
/**
* Check if account can be deleted
* @param userId - User ID to check
* @param db - Authenticated database client
*/
export async function canDeleteAccount(
userId: string,
db: AuthenticatedDB,
): Promise<{ canDelete: boolean; reason?: string }> {
const activeBookings = await db.booking.count({
where: { userId, status: "ACTIVE" }
});
return activeBookings === 0
? { canDelete: true }
: { canDelete: false, reason: "Active bookings exist" };
}
Step 3: Update Caller Sites
Server actions that use getAuthContext() already have the authenticated db:
// In server action
import { getAuthContext } from "./utils";
import { canDeleteAccount } from "@/lib/utils/account-deletion";
export async function deleteAccount(data: unknown): Promise<ActionResult> {
const auth = await getAuthContext();
if (!auth.authenticated) return auth;
// Pass the authenticated db client
const canDelete = await canDeleteAccount(auth.user.id, auth.db);
if (!canDelete.canDelete) {
return { success: false, error: canDelete.reason };
}
// ... proceed with deletion
}
Step 4: Handle Transaction Callbacks
For functions using $transaction, the transaction callback receives the same type:
export async function softDeleteUser(
userId: string,
db: AuthenticatedDB
): Promise<void> {
// Transaction callback infers type from db parameter
await db.$transaction(async (tx) => {
// tx is automatically typed as AuthenticatedDB
await tx.user.update({
where: { id: userId },
data: { deletedAt: new Date() }
});
});
}
Verification
After applying this pattern:
- TypeScript Check: Run
turbo types- should pass with no TS2502 errors - Access Control: Verify that utility functions enforce ZenStack policies:
// This should throw/return empty if user lacks access const result = await myUtil(userId, auth.db); - Import Correctness: Check that utilities import
db as dbClient, notbaseDB
Why This Works
- Local Type Alias: Breaks the circular reference by creating an intermediate type
- Consistent Import Name: Using
db as dbClientprevents naming conflicts with the parameter - Access Control Preserved:
AuthenticatedDBtype maintains the$setAuth()wrapper, ensuring policies are enforced
Common Mistakes to Avoid
- Using
baseDBin utilities: Always acceptdbas parameter, never importbaseDBdirectly (bypasses access control) - Forgetting to update callers: All call sites must pass
auth.dborauthContext.db - Mixing authenticated/unauthenticated clients: Be consistent - utilities should always require authenticated clients
Example: Complete Refactor
Before (bypassing access control):
"use server";
import { baseDB } from "database";
export async function deleteUserData(userId: string) {
await baseDB.user.delete({ where: { id: userId } });
}
After (enforcing access control):
"use server";
import { db as dbClient } from "database";
type AuthenticatedDB = ReturnType<typeof dbClient.$setAuth>;
export async function deleteUserData(
userId: string,
db: AuthenticatedDB
) {
// Access control policies automatically enforced
await db.user.delete({ where: { id: userId } });
}
Caller (server action):
import { getAuthContext } from "./utils";
import { deleteUserData } from "@/lib/utils/data";
export async function deleteMyData(): Promise<ActionResult> {
const auth = await getAuthContext();
if (!auth.authenticated) return auth;
await deleteUserData(auth.user.id, auth.db);
return { success: true };
}
Notes
- This pattern applies to ZenStack 3.x with Better Auth integration
- The
AuthenticatedDBtype preserves all ZenStack enhanced methods (findMany, etc.) - Each utility file needs its own
type AuthenticatedDBdeclaration (not exported from database package) - If utility functions don't need access control, consider whether they should be server actions instead
References
Weekly Installs
2
Repository
hankanman/claude-configFirst Seen
Mar 4, 2026
Security Audits
Installed on
qoder2
gemini-cli2
claude-code2
github-copilot2
windsurf2
codex2