auth-setup
Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.
- License 许可
- AGPL-3.0
- 所属仓库
- baisharg/ASTN
- 最近更新
- 2026-11-04
- Stars
- ★ 2
- Forks
- 2
01 前言 Front Matter
- name
- auth-setup
- description
- Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.
02 资源文件 6
- 参考文档README.md1.8 KB
- 参考文档references/bash.md2.9 KB
- 参考文档references/rust.md2.4 KB
- 参考文档references/typescript.md343 B
- 参考文档references/ui.md633 B
- 脚本tests/shell-mktemp-root.test.sh9.8 KB
03 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 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
// 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']),
})
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()
}
Require Admin
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
// 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
},
})
Access Control Patterns
Owner-Only Access
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
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 ownership
if (task.userId !== user._id) {
throw new Error('You can only delete your own tasks')
}
await ctx.db.delete(args.taskId)
},
})
Team-Based Access
// 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,
})
},
})
Public vs Private Queries
Public Query (No Auth Required)
export 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()
},
})
Private Query (Auth Required)
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)
export 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()
}
},
})
Client Setup with WorkOS
WorkOS AuthKit provides a complete authentication solution with minimal setup.
React/Vite Setup
npm install @workos-inc/authkit-react
// 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>
);
}
Next.js Setup
npm install @workos-inc/authkit-nextjs
// 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>
);
}
// 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
# .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
Call storeUser on Sign-In
// 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
}
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.)
04 同仓库更多 Skills
schema-builder
Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.
integration-tanstack-start
PostHog integration for TanStack Start full-stack applications
migration-helper
Plan and execute Convex schema migrations safely, including adding fields, creating tables, and data transformations. Use when schema changes affect existing data.
gsd:sprint
Autonomous phase execution with Codex validation. Runs plan/execute/verify cycles in a bash loop until done or halted.
gsd:second-opinion
Get a second opinion from OpenAI Codex (GPT-5.2-codex) on code, architecture, or implementation decisions. Use when you want independent verification or an alternative perspective.
gsd:milestone-sprint
Run entire milestone autonomously with Codex validation. Auto-detects current milestone, executes all phases, and runs audit.