Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Initialize a new Convex backend from scratch with schema, auth, and basic CRUD operations. Use when starting a new project or adding Convex to an existing app.
.claude/skills/kunanonj-cursor-plugin-convex-convex-quickstart/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 86% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 163% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 183% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 116% | 0% |
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.
Before starting, verify:
bashnode --version # v18 or higher npm --version # v8 or higher
bash# Install Convex npm install convex # Initialize (creates convex/ directory) npx convex dev
This command:
convex/ directoryCreate convex/schema.ts:
typescriptimport { 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"]), });
We'll use WorkOS AuthKit, which provides a complete auth solution with minimal setup.
bashnpm install @workos-inc/authkit-react
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> ); }
bashnpm install @workos-inc/authkit-nextjs
typescript// 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# .env.local VITE_CONVEX_URL=https://your-deployment.convex.cloud VITE_WORKOS_CLIENT_ID=your_workos_client_id # For 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.
Create convex/lib/auth.ts:
typescriptimport { 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.ts:
typescriptimport { 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 ?? "", }); }, });
Create convex/tasks.ts:
typescriptimport { 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); }, });
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> ); }
For Development (use this!):
bash# Start development server (NOT production!) npx convex dev # This runs locally and auto-reloads on changes # Use this for all development work
For Production Deployment:
bash# ONLY use this when deploying to production! 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.
typescriptexport 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; }, });
typescriptexport 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// 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); } }, });
bashnpm create vite@latest my-app -- --template react-ts cd my-app npm install convex @workos-inc/authkit-react npx convex dev
bashnpx create-next-app@latest my-app cd my-app npm install convex @workos-inc/authkit-nextjs npx convex dev
bashnpx create-expo-app my-app cd my-app npm install convex npx convex dev
npm install convex completednpx convex dev running (use this, NOT deploy!)getCurrentUser helper implementednpx convex deployRemember: Use npx convex dev for all development work. Only use npx convex deploy when deploying to production!
After quickstart:
storeUser mutation on first sign-ingetCurrentUser is imported correctlynpx convex dev (regenerates types).withIndex() instead of .filter()| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 11,303 | 6,375 | -44% | 1 | 1 | 0% | 2,817 | 5,242 | +86% | 0 | 0 | — |
case-02 | fail→pass | 13,044 | 8,304 | -36% | 1 | 1 | 0% | 3,568 | 5,950 | +67% | 0 | 0 | — |
case-03 | pass→pass | 9,361 | 4,904 | -48% | 1 | 1 | 0% | 2,226 | 4,800 | +116% | 0 | 0 | — |
case-04 | pass→pass | 5,276 | 4,749 | -10% | 1 | 1 | 0% | 1,101 | 4,581 | +316% | 0 | 0 | — |
case-05 | fail→fail | 16,266 | 7,975 | -51% | 1 | 1 | 0% | 3,685 | 5,722 | +55% | 0 | 0 | — |
case-06 | pass→pass | 9,135 | 7,274 | -20% | 1 | 1 | 0% | 1,834 | 5,294 | +189% | 0 | 0 | — |
case-07 | pass→pass | 8,186 | 7,393 | -10% | 1 | 1 | 0% | 1,958 | 5,622 | +187% | 0 | 0 | — |
case-08 | fail→pass | 9,234 | 5,578 | -40% | 1 | 1 | 0% | 1,892 | 4,969 | +163% | 0 | 0 | — |
case-09 | pass→pass | 8,241 | 6,223 | -24% | 1 | 1 | 0% | 2,084 | 5,069 | +143% | 0 | 0 | — |
case-10 | pass→pass | 9,043 | 7,726 | -15% | 1 | 1 | 0% | 1,981 | 5,265 | +166% | 0 | 0 | — |
case-11 | pass→pass | 7,794 | 6,256 | -20% | 1 | 1 | 0% | 1,745 | 5,108 | +193% | 0 | 0 | — |
case-12 | fail→fail | 9,670 | 5,993 | -38% | 1 | 1 | 0% | 1,822 | 5,069 | +178% | 0 | 0 | — |
case-13 | pass→pass | 6,781 | 5,468 | -19% | 1 | 1 | 0% | 1,518 | 4,897 | +223% | 0 | 0 | — |
case-14 | fail→pass | 11,725 | 11,470 | -2% | 1 | 1 | 0% | 2,306 | 6,526 | +183% | 0 | 0 | — |
case-15 | pass→pass | 4,629 | 1,116 | -76% | 1 | 1 | 0% | 966 | 3,865 | +300% | 0 | 0 | — |
case-16 | pass→pass | 4,913 | 4,071 | -17% | 1 | 1 | 0% | 1,024 | 4,456 | +335% | 0 | 0 | — |
case-17 | pass→pass | 6,434 | 3,965 | -38% | 1 | 1 | 0% | 1,383 | 4,550 | +229% | 0 | 0 | — |
case-18 | fail→fail | 12,363 | 12,316 | -0% | 1 | 1 | 0% | 2,618 | 6,328 | +142% | 0 | 0 | — |
case-19 | pass→pass | 7,744 | 7,871 | +2% | 1 | 1 | 0% | 1,640 | 5,304 | +223% | 0 | 0 | — |
case-20 | pass→pass | 9,294 | 7,330 | -21% | 1 | 1 | 0% | 1,949 | 5,445 | +179% | 0 | 0 | — |
case-21 | pass→pass | 7,290 | 6,523 | -11% | 1 | 1 | 0% | 1,667 | 5,088 | +205% | 0 | 0 | — |
case-22 | pass→pass | 9,160 | 4,674 | -49% | 1 | 1 | 0% | 1,857 | 4,752 | +156% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.