Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.
.claude/skills/kunanonj-cursor-plugin-convex-auth-setup/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 114% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 148% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 134% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 138% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 217% | 0% |
Implement secure authentication in Convex with user management and access control.
Convex authentication has two main parts:
typescript// convex/schema.ts import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ users: defineTable({ // From auth provider identity tokenIdentifier: v.string(), // Unique per auth provider // User profile data name: v.string(), email: v.string(), pictureUrl: v.optional(v.string()), // Your app-specific fields role: v.union( v.literal("user"), v.literal("admin") ), createdAt: v.number(), updatedAt: v.optional(v.number()), }) .index("by_token", ["tokenIdentifier"]) .index("by_email", ["email"]), });
typescript// convex/lib/auth.ts import { QueryCtx, MutationCtx } from "./_generated/server"; import { Doc } from "./_generated/dataModel"; export async function getCurrentUser( ctx: QueryCtx | MutationCtx ): Promise<Doc<"users">> { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); } const user = await ctx.db .query("users") .withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier) ) .unique(); if (!user) { throw new Error("User not found"); } return user; } export async function getCurrentUserOrNull( ctx: QueryCtx | MutationCtx ): Promise<Doc<"users"> | null> { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return null; } return await ctx.db .query("users") .withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier) ) .unique(); }
typescriptexport async function requireAdmin( ctx: QueryCtx | MutationCtx ): Promise<Doc<"users">> { const user = await getCurrentUser(ctx); if (user.role !== "admin") { throw new Error("Admin access required"); } return user; }
typescript// convex/users.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; export const storeUser = mutation({ args: {}, handler: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); } // Check if user exists const existingUser = await ctx.db .query("users") .withIndex("by_token", q => q.eq("tokenIdentifier", identity.tokenIdentifier) ) .unique(); if (existingUser) { // Update last seen or other fields await ctx.db.patch(existingUser._id, { updatedAt: Date.now(), }); return existingUser._id; } // Create new user const userId = await ctx.db.insert("users", { tokenIdentifier: identity.tokenIdentifier, name: identity.name ?? "Anonymous", email: identity.email ?? "", pictureUrl: identity.pictureUrl, role: "user", createdAt: Date.now(), }); return userId; }, });
typescriptimport { mutation } from "./_generated/server"; import { v } from "convex/values"; import { getCurrentUser } from "./lib/auth"; export const updateProfile = mutation({ args: { name: v.string(), }, handler: async (ctx, args) => { const user = await getCurrentUser(ctx); await ctx.db.patch(user._id, { name: args.name, updatedAt: Date.now(), }); }, });
typescriptexport const deleteTask = mutation({ args: { taskId: v.id("tasks") }, handler: async (ctx, args) => { const user = await getCurrentUser(ctx); const task = await ctx.db.get(args.taskId); if (!task) { throw new Error("Task not found"); } // Check ownership if (task.userId !== user._id) { throw new Error("You can only delete your own tasks"); } await ctx.db.delete(args.taskId); }, });
typescript// Schema includes membership table export default defineSchema({ teams: defineTable({ name: v.string(), ownerId: v.id("users"), }), teamMembers: defineTable({ teamId: v.id("teams"), userId: v.id("users"), role: v.union(v.literal("owner"), v.literal("member")), }) .index("by_team", ["teamId"]) .index("by_user", ["userId"]) .index("by_team_and_user", ["teamId", "userId"]), }); // Helper to check team access async function requireTeamAccess( ctx: MutationCtx, teamId: Id<"teams"> ): Promise<{ user: Doc<"users">, membership: Doc<"teamMembers"> }> { const user = await getCurrentUser(ctx); const membership = await ctx.db .query("teamMembers") .withIndex("by_team_and_user", q => q.eq("teamId", teamId).eq("userId", user._id) ) .unique(); if (!membership) { throw new Error("You don't have access to this team"); } return { user, membership }; } // Use in functions export const createProject = mutation({ args: { teamId: v.id("teams"), name: v.string(), }, handler: async (ctx, args) => { await requireTeamAccess(ctx, args.teamId); return await ctx.db.insert("projects", { teamId: args.teamId, name: args.name, }); }, });
typescriptexport const listPublicPosts = query({ args: {}, handler: async (ctx) => { // No auth check - anyone can read return await ctx.db .query("posts") .withIndex("by_published", q => q.eq("published", true)) .collect(); }, });
typescriptexport const getMyPosts = query({ args: {}, handler: async (ctx) => { const user = await getCurrentUser(ctx); return await ctx.db .query("posts") .withIndex("by_user", q => q.eq("userId", user._id)) .collect(); }, });
typescriptexport const getPosts = query({ args: {}, handler: async (ctx) => { const user = await getCurrentUserOrNull(ctx); if (user) { // Show all posts including drafts for this user return await ctx.db .query("posts") .withIndex("by_user", q => q.eq("userId", user._id)) .collect(); } else { // Show only public posts for anonymous users return await ctx.db .query("posts") .withIndex("by_published", q => q.eq("published", true)) .collect(); } }, });
WorkOS AuthKit provides a complete authentication solution with minimal setup.
bashnpm install @workos-inc/authkit-react
typescript// src/main.tsx import { AuthKitProvider, useAuth } from "@workos-inc/authkit-react"; import { ConvexReactClient } from "convex/react"; import { ConvexProvider } from "convex/react"; const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); // Configure Convex to use WorkOS auth convex.setAuth(useAuth); function App() { return ( <AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}> <ConvexProvider client={convex}> <YourApp /> </ConvexProvider> </AuthKitProvider> ); }
bashnpm install @workos-inc/authkit-nextjs
typescript// app/layout.tsx import { AuthKitProvider } from "@workos-inc/authkit-nextjs"; import { ConvexClientProvider } from "./ConvexClientProvider"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html> <body> <AuthKitProvider> <ConvexClientProvider> {children} </ConvexClientProvider> </AuthKitProvider> </body> </html> ); }
typescript// app/ConvexClientProvider.tsx "use client"; import { ConvexReactClient } from "convex/react"; import { ConvexProvider } from "convex/react"; import { useAuth } from "@workos-inc/authkit-nextjs"; const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); export function ConvexClientProvider({ children }: { children: React.ReactNode }) { const { getToken } = useAuth(); convex.setAuth(async () => { return await getToken(); }); return <ConvexProvider client={convex}>{children}</ConvexProvider>; }
bash# .env.local (React/Vite) VITE_CONVEX_URL=https://your-deployment.convex.cloud VITE_WORKOS_CLIENT_ID=your_workos_client_id # .env.local (Next.js) NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud NEXT_PUBLIC_WORKOS_CLIENT_ID=your_workos_client_id WORKOS_API_KEY=your_workos_api_key WORKOS_COOKIE_PASSWORD=generate_a_random_32_character_string
typescript// In your app after user signs in import { useMutation } from "convex/react"; import { api } from "../convex/_generated/api"; import { useEffect } from "react"; import { useAuth } from "@workos-inc/authkit-react"; function YourApp() { const { user } = useAuth(); const storeUser = useMutation(api.users.storeUser); useEffect(() => { if (user) { storeUser(); } }, [user, storeUser]); // ... rest of your app }
If you need to use a different provider, see the Convex auth documentation for:
tokenIdentifier indexgetCurrentUser helper functionstoreUser mutation for first sign-in| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-07 | fail→pass | 8,603 | 5,984 | -30% | 1 | 1 | 0% | 2,081 | 4,453 | +114% | 0 | 0 | — |
case-01 | fail→pass | 7,907 | 7,432 | -6% | 1 | 1 | 0% | 2,056 | 5,108 | +148% | 0 | 0 | — |
case-02 | fail→fail | 12,212 | 10,331 | -15% | 1 | 1 | 0% | 3,305 | 5,813 | +76% | 0 | 0 | — |
case-03 | fail→fail | 11,502 | 10,384 | -10% | 1 | 1 | 0% | 2,936 | 5,752 | +96% | 0 | 0 | — |
case-04 | pass→pass | 8,467 | 4,938 | -42% | 1 | 1 | 0% | 2,042 | 4,266 | +109% | 0 | 0 | — |
case-05 | fail→pass | 7,869 | 5,296 | -33% | 1 | 1 | 0% | 1,885 | 4,412 | +134% | 0 | 0 | — |
case-06 | pass→pass | 10,186 | 8,503 | -17% | 1 | 1 | 0% | 2,478 | 5,281 | +113% | 0 | 0 | — |
case-08 | fail→fail | 11,184 | 7,098 | -37% | 1 | 1 | 0% | 2,754 | 4,780 | +74% | 0 | 0 | — |
case-09 | pass→pass | 8,279 | 3,556 | -57% | 1 | 1 | 0% | 1,781 | 3,807 | +114% | 0 | 0 | — |
case-10 | pass→pass | 10,355 | 8,835 | -15% | 1 | 1 | 0% | 2,471 | 5,328 | +116% | 0 | 0 | — |
case-11 | fail→pass | 6,710 | 2,501 | -63% | 1 | 1 | 0% | 1,504 | 3,576 | +138% | 0 | 0 | — |
case-12 | fail→pass | 6,016 | 5,632 | -6% | 1 | 1 | 0% | 1,384 | 4,382 | +217% | 0 | 0 | — |
case-13 | fail→pass | 5,450 | 4,921 | -10% | 1 | 1 | 0% | 1,356 | 3,941 | +191% | 0 | 0 | — |
case-14 | pass→pass | 8,507 | 4,863 | -43% | 1 | 1 | 0% | 1,872 | 4,031 | +115% | 0 | 0 | — |
case-15 | fail→pass | 4,468 | 4,306 | -4% | 1 | 1 | 0% | 1,061 | 4,004 | +277% | 0 | 0 | — |
case-16 | fail→fail | 7,890 | 6,847 | -13% | 1 | 1 | 0% | 1,939 | 4,827 | +149% | 0 | 0 | — |
case-17 | fail→pass | 10,018 | 4,687 | -53% | 1 | 1 | 0% | 2,297 | 4,012 | +75% | 0 | 0 | — |
case-18 | pass→pass | 4,179 | 1,105 | -74% | 1 | 1 | 0% | 773 | 3,152 | +308% | 0 | 0 | — |
case-19 | fail→pass | 8,303 | 5,491 | -34% | 1 | 1 | 0% | 1,624 | 4,116 | +153% | 0 | 0 | — |
case-20 | pass→pass | 12,901 | 11,413 | -12% | 1 | 1 | 0% | 3,119 | 6,112 | +96% | 0 | 0 | — |
case-21 | pass→pass | 13,245 | 13,039 | -2% | 1 | 1 | 0% | 3,200 | 6,345 | +98% | 0 | 0 | — |
case-22 | pass→pass | 14,994 | 10,720 | -29% | 1 | 1 | 0% | 3,863 | 5,715 | +48% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.