Skip to content
Back to skills

Clerk Auth Atemndobs Nebula Rfp

ASecurity

Clerk authentication patterns and integration with Convex. Use when implementing sign-in/sign-out, protected routes, user profile sync, or role-based access control.

  • 666 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added February 8, 2026
developmenttypescriptgobashreactnodegitapibackend

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned February 12, 2026

npx -y skills add majiayu000/claude-skill-registry --skill clerk-auth-atemndobs-nebula-rfp --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Clerk Auth Atemndobs Nebula Rfp?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Clerk Auth Atemndobs Nebula Rfp
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-clerk-auth-atemndobs-nebula-rfp/badge)](https://www.skillsdirectory.com/skills/majiayu000-clerk-auth-atemndobs-nebula-rfp)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: clerk-auth
description: Clerk authentication patterns and integration with Convex. Use when implementing sign-in/sign-out, protected routes, user profile sync, or role-based access control.
allowed-tools: Read, Grep, Glob
---

# Clerk Authentication Skill

## Overview

This skill provides patterns for integrating Clerk authentication with the RFP Discovery platform and Convex backend.

## Setup

### Install Dependencies

```bash
npm install @clerk/clerk-react convex
```

### Environment Variables

```env
# .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

1. Create application at https://dashboard.clerk.com
2. Configure sign-in methods (Email, Google, GitHub)
3. Create JWT template for Convex:
   - Name: `convex`
   - Claims:
     ```json
     {
       "aud": "convex",
       "sub": "{{user.id}}",
       "name": "{{user.full_name}}",
       "email": "{{user.primary_email_address}}",
       "picture": "{{user.image_url}}"
     }
     ```

### Convex Auth Config

```typescript
// convex/auth.config.ts
export default {
  providers: [
    {
      domain: process.env.CLERK_ISSUER_URL,
      applicationID: "convex",
    },
  ],
};
```

## Provider Setup

### App Entry Point

```tsx
// 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

```tsx
// 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

```tsx
// 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)

```tsx
// 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

```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 <AppContent />;
}
```

## Header Integration

```tsx
// 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

```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 (
    <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 |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.