);
}
if (!isSignedIn) {
return ;
}
// Role check would use Convex query here
return <>{children}>;
}
```
### Auth Guard (Simple)
```tsx
// components/AuthGuard.tsx
import { SignedIn, SignedOut, RedirectToSignIn } from "@clerk/clerk-react";
export function AuthGuard({ children }: { children: React.ReactNode }) {
return (
<>
{children}
>
);
}
```
## Convex Auth Patterns
### User Identity in Mutations
```typescript
// convex/pursuits.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const create = mutation({
args: { rfpId: v.id("rfps") },
handler: async (ctx, args) => {
// Always check auth first
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
return await ctx.db.insert("pursuits", {
rfpId: args.rfpId,
userId: identity.subject, // Clerk user ID
userName: identity.name ?? "Unknown",
userEmail: identity.email ?? "",
status: "new",
createdAt: Date.now(),
updatedAt: Date.now(),
});
},
});
```
### User Sync on First Sign-In
```typescript
// convex/users.ts
import { mutation, query } from "./_generated/server";
export const syncUser = mutation({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const existing = await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
.first();
if (existing) {
// Update existing user
await ctx.db.patch(existing._id, {
name: identity.name ?? existing.name,
email: identity.email ?? existing.email,
imageUrl: identity.pictureUrl,
updatedAt: Date.now(),
});
return existing._id;
}
// Create new user with default role
return await ctx.db.insert("users", {
clerkId: identity.subject,
name: identity.name ?? "",
email: identity.email ?? "",
imageUrl: identity.pictureUrl,
role: "user", // Default role
createdAt: Date.now(),
updatedAt: Date.now(),
});
},
});
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
.first();
},
});
```
### Auth Helper Functions
```typescript
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "../_generated/server";
export async function requireAuth(ctx: QueryCtx | MutationCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated");
}
return identity;
}
export async function requireAdmin(ctx: QueryCtx | MutationCtx) {
const identity = await requireAuth(ctx);
const user = await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
.first();
if (!user || user.role !== "admin") {
throw new Error("Admin access required");
}
return { identity, user };
}
export async function getOptionalUser(ctx: QueryCtx) {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
.first();
}
```
### Admin-Only Mutation
```typescript
// convex/admin.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { requireAdmin } from "./lib/auth";
export const deleteRfp = mutation({
args: { rfpId: v.id("rfps") },
handler: async (ctx, args) => {
await requireAdmin(ctx); // Throws if not admin
await ctx.db.delete(args.rfpId);
return { success: true };
},
});
export const updateUserRole = mutation({
args: {
userId: v.id("users"),
role: v.string(),
},
handler: async (ctx, args) => {
const { user: adminUser } = await requireAdmin(ctx);
// Prevent self-demotion
if (args.userId === adminUser._id) {
throw new Error("Cannot change your own role");
}
await ctx.db.patch(args.userId, {
role: args.role,
updatedAt: Date.now(),
});
return { success: true };
},
});
```
## React Hooks
### useCurrentUser Hook
```tsx
// hooks/useCurrentUser.ts
import { useQuery } from "convex/react";
import { useUser, useAuth } from "@clerk/clerk-react";
import { api } from "../convex/_generated/api";
export function useCurrentUser() {
const { user: clerkUser, isLoaded: clerkLoaded } = useUser();
const { isSignedIn } = useAuth();
const convexUser = useQuery(
api.users.getCurrentUser,
isSignedIn ? {} : "skip"
);
return {
clerkUser,
convexUser,
isLoaded: clerkLoaded && (convexUser !== undefined || !isSignedIn),
isSignedIn: !!clerkUser,
isAdmin: convexUser?.role === "admin",
userId: convexUser?._id,
};
}
```
### Auto-Sync User Hook
```tsx
// hooks/useSyncUser.ts
import { useEffect } from "react";
import { useMutation } from "convex/react";
import { useAuth } from "@clerk/clerk-react";
import { api } from "../convex/_generated/api";
export function useSyncUser() {
const { isSignedIn, isLoaded } = useAuth();
const syncUser = useMutation(api.users.syncUser);
useEffect(() => {
if (isLoaded && isSignedIn) {
syncUser().catch(console.error);
}
}, [isLoaded, isSignedIn, syncUser]);
}
// Use in App.tsx
function App() {
useSyncUser(); // Syncs user on sign-in
return ;
}
```
## Header Integration
```tsx
// components/Header.tsx
import { AuthButtons } from "./AuthButtons";
import { useCurrentUser } from "../hooks/useCurrentUser";
export function Header() {
const { convexUser, isAdmin, isLoaded } = useCurrentUser();
return (
RFP Discovery
{isAdmin && (
Admin
)}
{isLoaded && convexUser && (
{convexUser.name}
)}
);
}
```
## Role-Based UI
```tsx
// components/AdminSection.tsx
import { useCurrentUser } from "../hooks/useCurrentUser";
export function AdminSection({ children }: { children: React.ReactNode }) {
const { isAdmin, isLoaded } = useCurrentUser();
if (!isLoaded) return null;
if (!isAdmin) return null;
return <>{children}>;
}
// Usage
function Dashboard() {
return (
Dashboard
{/* Visible to all */}
{/* Admin only */}
);
}
```
## Common Patterns Summary
| Pattern | Use Case |
|---------|----------|
| `SignedIn` / `SignedOut` | Conditional rendering based on auth |
| `useAuth().isSignedIn` | Check auth state in hooks |
| `ctx.auth.getUserIdentity()` | Get user in Convex functions |
| `requireAuth(ctx)` | Throw if not authenticated |
| `requireAdmin(ctx)` | Throw if not admin |
| User sync mutation | Keep Convex user in sync with Clerk |