<< All versions
Skill v1.0.1
currentAutomated scan100/100get-convex/convex-agent-plugins/auth-setup
1 files
──Details
PublishedJune 17, 2026 at 08:34 AM
Content Hashsha256:c916a4df1f937ef1...
Git SHAf104efb49a78
Bump Typepatch
──Files
Files (1 file, 10.3 KB)
SKILL.md10.3 KBactive
SKILL.md · 463 lines · 10.3 KB
version: "1.0.1" name: auth-setup description: Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.
Convex Authentication Setup
Implement secure authentication in Convex with user management and access control.
When to Use
- Setting up authentication for the first time
- Implementing user management (users table, identity mapping)
- Creating authentication helper functions
- Setting up OAuth providers (WorkOS, Auth0, etc.)
Architecture Overview
Convex authentication has two main parts:
- Client Authentication: Use a provider (WorkOS, Auth0, custom JWT)
- Backend Identity: Map auth provider identity to your users table
Schema Setup
typescript
// convex/schema.tsimport { defineSchema, defineTable } from "convex/server";import { v } from "convex/values";export default defineSchema({users: defineTable({// From auth provider identitytokenIdentifier: v.string(), // Unique per auth provider// User profile dataname: v.string(),email: v.string(),pictureUrl: v.optional(v.string()),// Your app-specific fieldsrole: v.union(v.literal("user"),v.literal("admin")),createdAt: v.number(),updatedAt: v.optional(v.number()),}).index("by_token", ["tokenIdentifier"]).index("by_email", ["email"]),});
Core Helper Functions
Get Current User
typescript
// convex/lib/auth.tsimport { 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();}
Require Admin
typescript
export 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;}
User Creation/Upsert
On First Sign-In
typescript
// convex/users.tsimport { 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 existsconst existingUser = await ctx.db.query("users").withIndex("by_token", q =>q.eq("tokenIdentifier", identity.tokenIdentifier)).unique();if (existingUser) {// Update last seen or other fieldsawait ctx.db.patch(existingUser._id, {updatedAt: Date.now(),});return existingUser._id;}// Create new userconst 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;},});
Access Control Patterns
Owner-Only Access
typescript
import { 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(),});},});
Resource Ownership
typescript
export 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 ownershipif (task.userId !== user._id) {throw new Error("You can only delete your own tasks");}await ctx.db.delete(args.taskId);},});
Team-Based Access
typescript
// Schema includes membership tableexport 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 accessasync 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 functionsexport 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,});},});
Public vs Private Queries
Public Query (No Auth Required)
typescript
export const listPublicPosts = query({args: {},handler: async (ctx) => {// No auth check - anyone can readreturn await ctx.db.query("posts").withIndex("by_published", q => q.eq("published", true)).collect();},});
Private Query (Auth Required)
typescript
export 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();},});
Hybrid Query (Optional Auth)
typescript
export const getPosts = query({args: {},handler: async (ctx) => {const user = await getCurrentUserOrNull(ctx);if (user) {// Show all posts including drafts for this userreturn await ctx.db.query("posts").withIndex("by_user", q => q.eq("userId", user._id)).collect();} else {// Show only public posts for anonymous usersreturn await ctx.db.query("posts").withIndex("by_published", q => q.eq("published", true)).collect();}},});
Client Setup with WorkOS
WorkOS AuthKit provides a complete authentication solution with minimal setup.
React/Vite Setup
bash
npm install @workos-inc/authkit-react
typescript
// src/main.tsximport { 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 authconvex.setAuth(useAuth);function App() {return (<AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}><ConvexProvider client={convex}><YourApp /></ConvexProvider></AuthKitProvider>);}
Next.js Setup
bash
npm install @workos-inc/authkit-nextjs
typescript
// app/layout.tsximport { 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>;}
Environment Variables
bash
# .env.local (React/Vite)VITE_CONVEX_URL=https://your-deployment.convex.cloudVITE_WORKOS_CLIENT_ID=your_workos_client_id# .env.local (Next.js)NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloudNEXT_PUBLIC_WORKOS_CLIENT_ID=your_workos_client_idWORKOS_API_KEY=your_workos_api_keyWORKOS_COOKIE_PASSWORD=generate_a_random_32_character_string
Call storeUser on Sign-In
typescript
// In your app after user signs inimport { 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}
Alternative Auth Providers
If you need to use a different provider, see the Convex auth documentation for:
- Custom JWT
- Auth0
- Other OAuth providers
Checklist
- [ ] Users table with
tokenIdentifierindex - [ ]
getCurrentUserhelper function - [ ]
storeUsermutation for first sign-in - [ ] Authentication check in all protected functions
- [ ] Authorization check for resource access
- [ ] Clear error messages ("Not authenticated", "Unauthorized")
- [ ] Client auth provider configured (WorkOS, Auth0, etc.)