clerk-auth
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 |
|---|---|
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 |
More from atemndobs/nebula-rfp
rfp-evaluate
Evaluate RFP opportunities using the 6-dimension scoring framework. Use when modifying evaluation criteria, adjusting keyword weights, or implementing AI-based evaluation.
11pursuit-brief
Generate 1-page pursuit briefs for qualified RFP opportunities. Use when creating bid/no-bid decision documents or implementing pursuit brief generation features.
5csv-export
Export RFP data, evaluations, and pursuits in CSV and other formats. Use when implementing data export features, building reports, or extracting data for analysis.
5proposal-builder
Assemble proposals from templates and content library. Use when implementing proposal generation, managing content blocks, or working with proposal templates.
5learning
Explain advanced technical decisions and implementations from the current session. Tailored for Manifesting Generator learning style - concrete patterns, alternatives, and actionable next steps.
4convex-patterns
Convex database patterns and best practices for RFP Discovery. Use when writing Convex queries, mutations, actions, or schema definitions. Also helpful for real-time subscriptions and auth integration.
4