clerk-auth

Clerk Authentication Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "clerk-auth" with this command: npx skills add atemndobs/nebula-rfp/atemndobs-nebula-rfp-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}}" }

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 */}
  &#x3C;RfpList />

  {/* Admin only */}
  &#x3C;AdminSection>
    &#x3C;AdminControls />
  &#x3C;/AdminSection>
&#x3C;/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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

pursuit-brief

No summary provided by upstream source.

Repository SourceNeeds Review
General

rfp-evaluate

No summary provided by upstream source.

Repository SourceNeeds Review
General

csv-export

No summary provided by upstream source.

Repository SourceNeeds Review
General

proposal-builder

No summary provided by upstream source.

Repository SourceNeeds Review