Claude-skill-registry clerk-auth
Clerk authentication patterns and integration with Convex. Use when implementing sign-in/sign-out, protected routes, user profile sync, or role-based access control.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clerk-auth-atemndobs-nebula-rfp" ~/.claude/skills/majiayu000-claude-skill-registry-clerk-auth && rm -rf "$T"
manifest:
skills/data/clerk-auth-atemndobs-nebula-rfp/SKILL.mdsource content
Clerk Authentication Skill
Overview
This skill provides patterns for integrating Clerk authentication with the RFP Discovery platform and Convex backend.
Setup
Install Dependencies
npm install @clerk/clerk-react convex
Environment Variables
# .env.local (client-side) VITE_CLERK_PUBLISHABLE_KEY=pk_test_... VITE_CONVEX_URL=https://your-project.convex.cloud # Convex Dashboard (server-side) CLERK_ISSUER_URL=https://your-clerk-domain.clerk.accounts.dev
Clerk Dashboard Configuration
- Create application at https://dashboard.clerk.com
- Configure sign-in methods (Email, Google, GitHub)
- Create JWT template for Convex:
- Name:
convex - Claims:
{ "aud": "convex", "sub": "{{user.id}}", "name": "{{user.full_name}}", "email": "{{user.primary_email_address}}", "picture": "{{user.image_url}}" }
- Name:
Convex Auth Config
// convex/auth.config.ts export default { providers: [ { domain: process.env.CLERK_ISSUER_URL, applicationID: "convex", }, ], };
Provider Setup
App Entry Point
// src/main.tsx import React from "react"; import ReactDOM from "react-dom/client"; import { ClerkProvider, useAuth } from "@clerk/clerk-react"; import { ConvexProviderWithClerk } from "convex/react-clerk"; import { ConvexReactClient } from "convex/react"; import App from "./App"; const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); ReactDOM.createRoot(document.getElementById("root")!).render( <React.StrictMode> <ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}> <ConvexProviderWithClerk client={convex} useAuth={useAuth}> <App /> </ConvexProviderWithClerk> </ClerkProvider> </React.StrictMode> );
Authentication Components
Sign In/Out Buttons
// components/AuthButtons.tsx import { SignedIn, SignedOut, SignInButton, SignUpButton, UserButton, } from "@clerk/clerk-react"; export function AuthButtons() { return ( <div className="flex items-center gap-4"> <SignedOut> <SignInButton mode="modal"> <button className="px-4 py-2 text-sm text-muted-foreground hover:text-foreground"> Sign In </button> </SignInButton> <SignUpButton mode="modal"> <button className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"> Sign Up </button> </SignUpButton> </SignedOut> <SignedIn> <UserButton afterSignOutUrl="/" appearance={{ elements: { avatarBox: "w-10 h-10", }, }} /> </SignedIn> </div> ); }
Protected Route Component
// components/ProtectedRoute.tsx import { useAuth } from "@clerk/clerk-react"; import { Navigate, useLocation } from "react-router-dom"; interface ProtectedRouteProps { children: React.ReactNode; requiredRole?: "admin" | "user"; } export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) { const { isLoaded, isSignedIn } = useAuth(); const location = useLocation(); if (!isLoaded) { return ( <div className="flex items-center justify-center h-screen"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" /> </div> ); } if (!isSignedIn) { return <Navigate to="/sign-in" state={{ from: location }} replace />; } // Role check would use Convex query here return <>{children}</>; }
Auth Guard (Simple)
// components/AuthGuard.tsx import { SignedIn, SignedOut, RedirectToSignIn } from "@clerk/clerk-react"; export function AuthGuard({ children }: { children: React.ReactNode }) { return ( <> <SignedIn>{children}</SignedIn> <SignedOut> <RedirectToSignIn /> </SignedOut> </> ); }
Convex Auth Patterns
User Identity in Mutations
// 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
// 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
// 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
// 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
// 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
// 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 <AppContent />; }
Header Integration
// components/Header.tsx import { AuthButtons } from "./AuthButtons"; import { useCurrentUser } from "../hooks/useCurrentUser"; export function Header() { const { convexUser, isAdmin, isLoaded } = useCurrentUser(); return ( <header className="flex items-center justify-between p-4 border-b border-border"> <div className="flex items-center gap-4"> <h1 className="text-xl font-bold">RFP Discovery</h1> {isAdmin && ( <span className="px-2 py-1 text-xs bg-primary/20 text-primary rounded"> Admin </span> )} </div> <div className="flex items-center gap-4"> {isLoaded && convexUser && ( <span className="text-sm text-muted-foreground"> {convexUser.name} </span> )} <AuthButtons /> </div> </header> ); }
Role-Based UI
// 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 ( <div> <h1>Dashboard</h1> {/* Visible to all */} <RfpList /> {/* Admin only */} <AdminSection> <AdminControls /> </AdminSection> </div> ); }
Common Patterns Summary
| Pattern | Use Case |
|---|---|
/ | Conditional rendering based on auth |
| Check auth state in hooks |
| Get user in Convex functions |
| Throw if not authenticated |
| Throw if not admin |
| User sync mutation | Keep Convex user in sync with Clerk |