convex-setup-auth
Installation
SKILL.md
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 auth providers (Convex Auth, Clerk, WorkOS AuthKit, Auth0, custom JWT)
First Step: Choose the Auth Provider
Do not assume a provider. Before writing setup code:
- Ask the user which auth solution they want, unless the repository already makes it obvious
- If the repo already uses a provider, continue with that provider unless the user wants to switch
- If the user has not chosen and the repo does not make it obvious, ask before proceeding
Common options:
- Convex Auth: good default when the user wants auth handled directly in Convex
- Clerk: use when the app already uses Clerk
- WorkOS AuthKit: use when the app already uses WorkOS
- Auth0: use when the app already uses Auth0
- Custom JWT provider: use when integrating an existing auth system
Look for signals in the repo before asking:
- Dependencies such as
@clerk/*,@workos-inc/*,@auth0/* - Existing files such as
convex/auth.config.ts, auth middleware, provider wrappers - Environment variables that clearly point at a provider
Schema Setup
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
tokenIdentifier: v.string(),
name: v.string(),
email: v.string(),
pictureUrl: v.optional(v.string()),
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"]),
});
Core Helper Functions
Get Current User
// 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();
}
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
// 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");
const existingUser = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (existingUser) {
await ctx.db.patch(existingUser._id, { updatedAt: Date.now() });
return existingUser._id;
}
return await ctx.db.insert("users", {
tokenIdentifier: identity.tokenIdentifier,
name: identity.name ?? "Anonymous",
email: identity.email ?? "",
pictureUrl: identity.pictureUrl,
role: "user",
createdAt: Date.now(),
});
},
});
Access Control Patterns
Resource Ownership
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");
if (task.userId !== user._id) throw new Error("You can only delete your own tasks");
await ctx.db.delete(args.taskId);
},
});
Team-Based 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 };
}
Checklist
- Chosen the correct auth provider before writing setup code
- 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
Weekly Installs
1
Repository
waynesutton/mar…own-siteGitHub Stars
609
First Seen
10 days ago
Security Audits