auth-setup
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvex Authentication Setup
Convex 认证配置
Implement secure authentication in Convex with user management and access control.
在Convex中实现安全认证,包含用户管理与访问控制。
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.)
- 首次配置认证功能
- 实现用户管理(用户表、身份映射)
- 创建认证辅助函数
- 配置OAuth提供商(WorkOS、Auth0等)
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
Convex认证包含两个主要部分:
- 客户端认证:使用认证提供商(WorkOS、Auth0、自定义JWT)
- 后端身份管理:将认证提供商的身份映射到你的用户表
Schema Setup
Schema 配置
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/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
获取当前用户
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();
}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();
}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;
}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.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;
},
});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");
}
// 检查用户是否已存在
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;
}
// 创建新用户
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
仅所有者访问
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(),
});
},
});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 ownership
if (task.userId !== user._id) {
throw new Error("You can only delete your own tasks");
}
await ctx.db.delete(args.taskId);
},
});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");
}
// 检查所有权
if (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 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,
});
},
});typescript
// Schema 包含成员表
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"]),
});
// 检查团队访问权限的辅助函数
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 };
}
// 在函数中使用
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)
公开查询(无需认证)
typescript
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();
},
});typescript
export const listPublicPosts = query({
args: {},
handler: async (ctx) => {
// 无认证检查 - 任何人都可读取
return 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();
},
});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 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();
}
},
});typescript
export const getPosts = query({
args: {},
handler: async (ctx) => {
const user = await getCurrentUserOrNull(ctx);
if (user) {
// 为当前用户显示所有帖子(包括草稿)
return await ctx.db
.query("posts")
.withIndex("by_user", q => q.eq("userId", user._id))
.collect();
} else {
// 为匿名用户仅显示公开帖子
return await ctx.db
.query("posts")
.withIndex("by_published", q => q.eq("published", true))
.collect();
}
},
});Client Setup with WorkOS
基于WorkOS的客户端配置
WorkOS AuthKit provides a complete authentication solution with minimal setup.
WorkOS AuthKit提供了一套完整的认证解决方案,且配置步骤极少。
React/Vite Setup
React/Vite 配置
bash
npm install @workos-inc/authkit-reacttypescript
// 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>
);
}bash
npm install @workos-inc/authkit-reacttypescript
// 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);
// 配置Convex使用WorkOS认证
convex.setAuth(useAuth);
function App() {
return (
<AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}>
<ConvexProvider client={convex}>
<YourApp />
</ConvexProvider>
</AuthKitProvider>
);
}Next.js Setup
Next.js 配置
bash
npm install @workos-inc/authkit-nextjstypescript
// 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
npm install @workos-inc/authkit-nextjstypescript
// 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>;
}Environment Variables
环境变量
bash
undefinedbash
undefined.env.local (React/Vite)
.env.local (React/Vite)
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=your_workos_client_id
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_WORKOS_CLIENT_ID=your_workos_client_id
.env.local (Next.js)
.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
undefinedNEXT_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
undefinedCall storeUser on Sign-In
登录时调用storeUser
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
}typescript
// 应用中用户登录后
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]);
// ... 应用其余部分
}Alternative Auth Providers
其他认证提供商
If you need to use a different provider, see the Convex auth documentation for:
- Custom JWT
- Auth0
- Other OAuth providers
如果你需要使用其他提供商,请查看Convex认证文档了解:
- 自定义JWT
- Auth0
- 其他OAuth提供商
Checklist
检查清单
- Users table with index
tokenIdentifier - helper function
getCurrentUser - mutation for first sign-in
storeUser - Authentication check in all protected functions
- Authorization check for resource access
- Clear error messages ("Not authenticated", "Unauthorized")
- Client auth provider configured (WorkOS, Auth0, etc.)
- 包含索引的用户表
tokenIdentifier - 辅助函数
getCurrentUser - 首次登录时的变更函数
storeUser - 所有受保护函数中包含认证检查
- 资源访问的授权检查
- 清晰的错误提示("未认证"、"无权限")
- 已配置客户端认证提供商(WorkOS、Auth0等)