Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Jackson57279/zapdev/llms.txt
Use this file to discover all available pages before exploring further.
Authentication
ZapDev uses Clerk for authentication with JWT (JSON Web Token) based authorization. All authenticated API requests require a valid Clerk JWT token.
Authentication Flow
- User signs in through Clerk (OAuth, email/password, etc.)
- Clerk generates a JWT token with user identity
- Token is included in API requests to authenticate the user
- Server validates the token and extracts user identity
- Protected endpoints verify user authorization
Clerk JWT Configuration
Convex Authentication
Convex is configured to accept Clerk JWTs through convex/auth.config.ts:
export default {
providers: [
{
domain: process.env.CLERK_JWT_ISSUER_DOMAIN!,
applicationID: "convex",
},
],
};
Environment Variables:
CLERK_JWT_ISSUER_DOMAIN: Your Clerk instance domain (e.g., clerk.your-app.com)
CLERK_JWT_TEMPLATE_NAME: JWT template name (default: "convex")
Server-Side Token Retrieval
The src/lib/auth-server.ts module provides utilities for retrieving and using Clerk tokens:
import { auth } from '@clerk/nextjs/server';
export async function getToken(): Promise<string | null> {
try {
const authResult = await auth();
const token = await authResult.getToken?.({
template: clerkJwtTemplate
});
return token ?? null;
} catch (error) {
console.error("Failed to get token:", error);
return null;
}
}
Authentication Helpers
Convex Authentication Helpers
Convex provides several authentication helpers in convex/helpers.ts:
getCurrentUserId(ctx)
Returns the current user’s Clerk ID or null if not authenticated.
export async function getCurrentUserId(
ctx: QueryCtx | MutationCtx
): Promise<string | null> {
const identity = await ctx.auth.getUserIdentity();
return identity?.subject || null;
}
Usage:
const userId = await getCurrentUserId(ctx);
if (!userId) {
// Handle unauthenticated case
}
requireAuth(ctx)
Requires authentication and throws an error if the user is not authenticated. Returns the user’s Clerk ID.
export async function requireAuth(
ctx: QueryCtx | MutationCtx
): Promise<string> {
const userId = await getCurrentUserId(ctx);
if (!userId) {
throw new Error("Unauthorized");
}
return userId;
}
Usage in Convex Functions (convex/projects.ts:287):
export const get = query({
args: {
projectId: v.id("projects"),
},
handler: async (ctx, args) => {
const userId = await requireAuth(ctx);
const project = await ctx.db.get(args.projectId);
if (!project) {
throw new Error("Project not found");
}
// Verify ownership
if (project.userId !== userId) {
throw new Error("Unauthorized");
}
return project;
},
});
Getting User Details
The getUser() function from src/lib/auth-server.ts:14 retrieves the current user’s information:
export async function getUser(): Promise<AuthenticatedUser | null> {
try {
const user = await currentUser();
if (!user) return null;
return {
id: user.id,
primaryEmail: user.primaryEmailAddress?.emailAddress ?? null,
displayName: user.fullName ?? user.username ?? user.firstName ?? null,
imageUrl: user.imageUrl ?? null,
};
} catch (error) {
console.error("Failed to get user:", error);
return null;
}
}
Type Definition:
type AuthenticatedUser = {
id: string; // Clerk user ID
primaryEmail: string | null; // User's email
displayName: string | null; // Display name
imageUrl: string | null; // Profile image URL
};
Authenticated Convex Client
For server-side operations, use the authenticated Convex client (src/lib/auth-server.ts:48):
export async function getConvexClientWithAuth() {
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!convexUrl) {
throw new Error("NEXT_PUBLIC_CONVEX_URL environment variable is not set");
}
const httpClient = new ConvexHttpClient(convexUrl);
const token = await getToken();
if (token) {
httpClient.setAuth(token);
}
return httpClient;
}
Usage in Server Components:
import { getConvexClientWithAuth } from '@/lib/auth-server';
import { api } from '@/convex/_generated/api';
export async function MyServerComponent() {
const convex = await getConvexClientWithAuth();
// All queries/mutations are authenticated
const projects = await convex.query(api.projects.list);
return <div>{/* ... */}</div>;
}
Authorization Patterns
Resource Ownership Verification
Always verify that the authenticated user owns the resource they’re accessing:
export const update = mutation({
args: {
projectId: v.id("projects"),
name: v.string(),
},
handler: async (ctx, args) => {
const userId = await requireAuth(ctx);
const project = await ctx.db.get(args.projectId);
if (!project) {
throw new Error("Project not found");
}
// Critical: Verify ownership
if (project.userId !== userId) {
throw new Error("Unauthorized");
}
await ctx.db.patch(args.projectId, {
name: args.name,
updatedAt: Date.now(),
});
return args.projectId;
},
});
Subscription-Based Access
Check subscription tiers using helper functions in convex/helpers.ts:20:
export async function hasProAccess(
ctx: QueryCtx | MutationCtx
): Promise<boolean> {
const userId = await getCurrentUserId(ctx);
if (!userId) return false;
const subscription = await ctx.db
.query("subscriptions")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.filter((q) => q.eq(q.field("status"), "active"))
.first();
if (!subscription) return false;
return subscription.productId ===
process.env.NEXT_PUBLIC_POLAR_PRO_PRODUCT_ID;
}
Other Helper Functions:
hasUnlimitedAccess(ctx): Check for unlimited tier
hasPlan(ctx, planProductId): Check for specific plan
hasFeature(ctx, featureId): Check for specific feature access
Security Best Practices
- Always Use Authentication Helpers: Use
requireAuth() in all protected Convex functions
- Verify Resource Ownership: Check that
project.userId === userId before mutations
- Never Expose Internal IDs: Don’t return Clerk user IDs in public responses
- Use Indexes for Queries: Filter by
userId using indexes, not .filter()
- Handle Token Expiry: Implement proper error handling for expired tokens
- Validate All Inputs: Use Convex validators (
v.*) for all function arguments
Error Handling
Unauthorized Errors
Convex:
if (!userId) {
throw new Error("Unauthorized");
}
Client-Side Handling
'use client';
import { useRouter } from 'next/navigation';
import { useQuery } from 'convex/react';
import { api } from '@/convex/_generated/api';
export function MyComponent() {
const router = useRouter();
const projects = useQuery(api.projects.list);
// Convex returns undefined for unauthenticated queries
// Redirect to sign-in if needed
if (projects === null) {
router.push('/sign-in');
}
// ... component logic
}
Next Steps