convex-quickstart
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvex Quickstart
Convex 快速入门
Get a production-ready Convex backend set up in minutes. This skill guides you through initializing Convex, creating your schema, setting up auth, and building your first CRUD operations.
几分钟内搭建好可用于生产环境的 Convex 后端。本指南将引导你完成 Convex 初始化、schema 创建、认证设置以及首个 CRUD 操作的构建。
When to Use
适用场景
- Starting a brand new project with Convex
- Adding Convex to an existing React/Next.js app
- Prototyping a new feature with real-time data
- Converting from another backend to Convex
- Teaching someone Convex for the first time
- 使用 Convex 启动全新项目
- 为现有 React/Next.js 应用添加 Convex
- 基于实时数据原型化新功能
- 从其他后端迁移至 Convex
- 首次向他人传授 Convex 使用方法
Prerequisites Check
前置检查
Before starting, verify:
bash
node --version # v18 or higher
npm --version # v8 or higher开始前,请确认:
bash
node --version # v18 或更高版本
npm --version # v8 或更高版本Quick Start Flow
快速入门流程
Step 1: Install and Initialize
步骤1:安装与初始化
bash
undefinedbash
undefinedInstall Convex
安装 Convex
npm install convex
npm install convex
Initialize (creates convex/ directory)
初始化(创建 convex/ 目录)
npx convex dev
This command:
- Creates `convex/` directory
- Sets up authentication
- Starts development server
- Generates TypeScript typesnpx convex dev
该命令会:
- 创建 `convex/` 目录
- 配置认证
- 启动开发服务器
- 生成 TypeScript 类型定义Step 2: Create Schema
步骤2:创建 Schema
Create :
convex/schema.tstypescript
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(),
}).index("by_token", ["tokenIdentifier"]),
// Add your tables here
// Example: Tasks table
tasks: defineTable({
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_completed", ["userId", "completed"]),
});创建 :
convex/schema.tstypescript
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(),
}).index("by_token", ["tokenIdentifier"]),
// 在此添加你的表
// 示例:任务表
tasks: defineTable({
userId: v.id("users"),
title: v.string(),
completed: v.boolean(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_and_completed", ["userId", "completed"]),
});Step 3: Set Up Authentication
步骤3:配置认证
We'll use WorkOS AuthKit, which provides a complete auth solution with minimal setup.
bash
npm install @workos-inc/authkit-react我们将使用 WorkOS AuthKit,它能以极少的配置提供完整的认证解决方案。
bash
npm install @workos-inc/authkit-reactFor React/Vite Apps:
针对 React/Vite 应用:
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>
);
}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);
// 配置 Convex 使用 WorkOS 认证
convex.setAuth(useAuth);
function App() {
return (
<AuthKitProvider clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}>
<ConvexProvider client={convex}>
<YourApp />
</ConvexProvider>
</AuthKitProvider>
);
}For Next.js Apps:
针对 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
.env.local
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
For Next.js:
针对 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
**Alternative auth providers:** If you need to use a different provider (Clerk, Auth0, custom JWT), see the [Convex auth documentation](https://docs.convex.dev/auth).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
**替代认证提供商:** 如果你需要使用其他提供商(Clerk、Auth0、自定义 JWT),请查看 [Convex 认证文档](https://docs.convex.dev/auth)。Step 4: Create Auth Helpers
步骤4:创建认证工具函数
Create :
convex/lib/auth.tstypescript
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;
}Create :
convex/users.tstypescript
import { mutation } from "./_generated/server";
export const store = mutation({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const existing = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (existing) return existing._id;
return await ctx.db.insert("users", {
tokenIdentifier: identity.tokenIdentifier,
name: identity.name ?? "Anonymous",
email: identity.email ?? "",
});
},
});创建 :
convex/lib/auth.tstypescript
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("未认证");
}
const user = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (!user) {
throw new Error("用户未找到");
}
return user;
}创建 :
convex/users.tstypescript
import { mutation } from "./_generated/server";
export const store = mutation({
args: {},
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("未认证");
const existing = await ctx.db
.query("users")
.withIndex("by_token", q =>
q.eq("tokenIdentifier", identity.tokenIdentifier)
)
.unique();
if (existing) return existing._id;
return await ctx.db.insert("users", {
tokenIdentifier: identity.tokenIdentifier,
name: identity.name ?? "匿名",
email: identity.email ?? "",
});
},
});Step 5: Create Your First CRUD Operations
步骤5:创建首个 CRUD 操作
Create :
convex/tasks.tstypescript
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getCurrentUser } from "./lib/auth";
// List all tasks for current user
export const list = query({
args: {},
handler: async (ctx) => {
const user = await getCurrentUser(ctx);
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", user._id))
.order("desc")
.collect();
},
});
// Get a single task
export const get = query({
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("Unauthorized");
return task;
},
});
// Create a task
export const create = mutation({
args: { title: v.string() },
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return await ctx.db.insert("tasks", {
userId: user._id,
title: args.title,
completed: false,
createdAt: Date.now(),
});
},
});
// Update a task
export const update = mutation({
args: {
taskId: v.id("tasks"),
title: v.optional(v.string()),
completed: v.optional(v.boolean()),
},
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("Unauthorized");
const updates: any = {};
if (args.title !== undefined) updates.title = args.title;
if (args.completed !== undefined) updates.completed = args.completed;
await ctx.db.patch(args.taskId, updates);
},
});
// Delete a task
export const remove = 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("Unauthorized");
await ctx.db.delete(args.taskId);
},
});创建 :
convex/tasks.tstypescript
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getCurrentUser } from "./lib/auth";
// 列出当前用户的所有任务
export const list = query({
args: {},
handler: async (ctx) => {
const user = await getCurrentUser(ctx);
return await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", user._id))
.order("desc")
.collect();
},
});
// 获取单个任务
export const get = query({
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("任务未找到");
if (task.userId !== user._id) throw new Error("无权限");
return task;
},
});
// 创建任务
export const create = mutation({
args: { title: v.string() },
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return await ctx.db.insert("tasks", {
userId: user._id,
title: args.title,
completed: false,
createdAt: Date.now(),
});
},
});
// 更新任务
export const update = mutation({
args: {
taskId: v.id("tasks"),
title: v.optional(v.string()),
completed: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx);
const task = await ctx.db.get(args.taskId);
if (!task) throw new Error("任务未找到");
if (task.userId !== user._id) throw new Error("无权限");
const updates: any = {};
if (args.title !== undefined) updates.title = args.title;
if (args.completed !== undefined) updates.completed = args.completed;
await ctx.db.patch(args.taskId, updates);
},
});
// 删除任务
export const remove = 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("任务未找到");
if (task.userId !== user._id) throw new Error("无权限");
await ctx.db.delete(args.taskId);
},
});Step 6: Use in Your React App
步骤6:在 React 应用中使用
typescript
// app/tasks/page.tsx
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
export default function TasksPage() {
const tasks = useQuery(api.tasks.list);
const create = useMutation(api.tasks.create);
const update = useMutation(api.tasks.update);
const remove = useMutation(api.tasks.remove);
if (!tasks) return <div>Loading...</div>;
return (
<div>
<h1>Tasks</h1>
{/* Create task */}
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
create({ title: formData.get("title") as string });
(e.target as HTMLFormElement).reset();
}}>
<input name="title" placeholder="New task" required />
<button type="submit">Add</button>
</form>
{/* Task list */}
{tasks.map(task => (
<div key={task._id}>
<input
type="checkbox"
checked={task.completed}
onChange={(e) => update({
taskId: task._id,
completed: e.target.checked
})}
/>
<span>{task.title}</span>
<button onClick={() => remove({ taskId: task._id })}>
Delete
</button>
</div>
))}
</div>
);
}typescript
// app/tasks/page.tsx
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";
export default function TasksPage() {
const tasks = useQuery(api.tasks.list);
const create = useMutation(api.tasks.create);
const update = useMutation(api.tasks.update);
const remove = useMutation(api.tasks.remove);
if (!tasks) return <div>加载中...</div>;
return (
<div>
<h1>任务列表</h1>
{/* 创建任务 */}
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
create({ title: formData.get("title") as string });
(e.target as HTMLFormElement).reset();
}}>
<input name="title" placeholder="新任务" required />
<button type="submit">添加</button>
</form>
{/* 任务列表 */}
{tasks.map(task => (
<div key={task._id}>
<input
type="checkbox"
checked={task.completed}
onChange={(e) => update({
taskId: task._id,
completed: e.target.checked
})}
/>
<span>{task.title}</span>
<button onClick={() => remove({ taskId: task._id })}>
删除
</button>
</div>
))}
</div>
);
}Step 7: Development vs Production
步骤7:开发环境 vs 生产环境
For Development (use this!):
bash
undefined开发环境(推荐使用!):
bash
undefinedStart development server (NOT production!)
启动开发服务器(请勿用于生产环境!)
npx convex dev
npx convex dev
This runs locally and auto-reloads on changes
本地运行,代码变更时自动重载
Use this for all development work
所有开发工作都请使用该命令
**For Production Deployment:**
```bash
**生产环境部署:**
```bashONLY use this when deploying to production!
仅在部署生产环境时使用!
npx convex deploy
npx convex deploy
WARNING: This deploys to your production environment
警告:该命令会部署至你的生产环境
Don't use this during development
开发期间请勿使用
**Important:** Always use `npx convex dev` during development. Only use `npx convex deploy` when you're ready to ship to production.
**重要提示:** 开发期间请始终使用 `npx convex dev`。只有在准备好发布生产版本时,才使用 `npx convex deploy`。Common Patterns
常见模式
Paginated Queries
分页查询
typescript
export const listPaginated = query({
args: {
cursor: v.optional(v.string()),
limit: v.number(),
},
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx);
const results = await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", user._id))
.order("desc")
.paginate({ cursor: args.cursor, limit: args.limit });
return results;
},
});typescript
export const listPaginated = query({
args: {
cursor: v.optional(v.string()),
limit: v.number(),
},
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx);
const results = await ctx.db
.query("tasks")
.withIndex("by_user", q => q.eq("userId", user._id))
.order("desc")
.paginate({ cursor: args.cursor, limit: args.limit });
return results;
},
});Filtering by Status
按状态筛选
typescript
export const listByStatus = query({
args: {
completed: v.boolean(),
},
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return await ctx.db
.query("tasks")
.withIndex("by_user_and_completed", q =>
q.eq("userId", user._id).eq("completed", args.completed)
)
.collect();
},
});typescript
export const listByStatus = query({
args: {
completed: v.boolean(),
},
handler: async (ctx, args) => {
const user = await getCurrentUser(ctx);
return await ctx.db
.query("tasks")
.withIndex("by_user_and_completed", q =>
q.eq("userId", user._id).eq("completed", args.completed)
)
.collect();
},
});Scheduled Jobs
定时任务
typescript
// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.daily(
"cleanup-old-tasks",
{ hourUTC: 0, minuteUTC: 0 },
internal.tasks.cleanupOld
);
export default crons;
// convex/tasks.ts
export const cleanupOld = internalMutation({
handler: async (ctx) => {
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const oldTasks = await ctx.db
.query("tasks")
.filter(q =>
q.and(
q.eq(q.field("completed"), true),
q.lt(q.field("createdAt"), thirtyDaysAgo)
)
)
.collect();
for (const task of oldTasks) {
await ctx.db.delete(task._id);
}
},
});typescript
// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.daily(
"cleanup-old-tasks",
{ hourUTC: 0, minuteUTC: 0 },
internal.tasks.cleanupOld
);
export default crons;
// convex/tasks.ts
export const cleanupOld = internalMutation({
handler: async (ctx) => {
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const oldTasks = await ctx.db
.query("tasks")
.filter(q =>
q.and(
q.eq(q.field("completed"), true),
q.lt(q.field("createdAt"), thirtyDaysAgo)
)
)
.collect();
for (const task of oldTasks) {
await ctx.db.delete(task._id);
}
},
});Project Templates
项目模板
React + Vite + Convex
React + Vite + Convex
bash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install convex @workos-inc/authkit-react
npx convex devbash
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install convex @workos-inc/authkit-react
npx convex devNext.js + Convex
Next.js + Convex
bash
npx create-next-app@latest my-app
cd my-app
npm install convex @workos-inc/authkit-nextjs
npx convex devbash
npx create-next-app@latest my-app
cd my-app
npm install convex @workos-inc/authkit-nextjs
npx convex devExpo (React Native) + Convex
Expo(React Native)+ Convex
bash
npx create-expo-app my-app
cd my-app
npm install convex
npx convex devbash
npx create-expo-app my-app
cd my-app
npm install convex
npx convex devChecklist
检查清单
- completed
npm install convex - running (use this, NOT deploy!)
npx convex dev - Schema created with proper indexes
- Auth provider configured (WorkOS/custom)
- helper implemented
getCurrentUser - User storage mutation created
- CRUD operations with auth checks
- Frontend integrated with hooks
- Tested queries and mutations locally
- When ready for production, use
npx convex deploy
Remember: Use for all development work. Only use when deploying to production!
npx convex devnpx convex deploy- 已完成
npm install convex - 正在运行(请使用该命令,而非 deploy!)
npx convex dev - 已创建包含正确索引的 Schema
- 已配置认证提供商(WorkOS/自定义)
- 已实现 工具函数
getCurrentUser - 已创建用户存储 mutation
- 已创建包含认证校验的 CRUD 操作
- 前端已通过 hooks 集成
- 已在本地测试查询和 mutation
- 准备生产部署时,使用
npx convex deploy
切记: 所有开发工作请使用 。仅在部署生产环境时使用 !
npx convex devnpx convex deployNext Steps
后续步骤
After quickstart:
- Add more tables to schema
- Implement relationships between tables
- Add file storage (Convex supports file uploads)
- Set up vector search for AI features
- Add cron jobs for scheduled tasks
- Configure production environment variables
完成快速入门后:
- 为 Schema 添加更多表
- 实现表之间的关联关系
- 添加文件存储(Convex 支持文件上传)
- 为 AI 功能设置向量搜索
- 添加定时任务
- 配置生产环境变量
Troubleshooting
故障排除
"Not authenticated" errors
“未认证”错误
- Ensure auth provider is configured
- Call mutation on first sign-in
storeUser - Check is imported correctly
getCurrentUser
- 确保认证提供商已正确配置
- 首次登录时调用 mutation
storeUser - 检查 是否已正确导入
getCurrentUser
Types not updating
类型未更新
- Run (regenerates types)
npx convex dev - Restart TypeScript server in editor
- 运行 (会重新生成类型)
npx convex dev - 重启编辑器中的 TypeScript 服务
Slow queries
查询缓慢
- Add indexes to schema
- Use instead of
.withIndex().filter() - Check query patterns in dashboard
- 为 Schema 添加索引
- 使用 替代
.withIndex().filter() - 在控制台中检查查询模式