Loading...
Loading...
Expert Convex backend development for December 2025. Use when (1) Building Convex queries, mutations, or actions, (2) Defining schemas and validators, (3) Integrating Convex with React/Next.js, (4) Implementing authentication with Convex Auth or Clerk, (5) Building AI agents with persistent memory, (6) Using file storage, scheduling, or workflows, (7) Optimizing database queries with indexes, or any Convex backend architecture questions.
npx skill4agent add ghostspeak/ghostspeak convex-expert-2025| Type | Purpose | Database | External APIs | Deterministic |
|---|---|---|---|---|
| Read data | ✅ Read | ❌ | ✅ Required |
| Write data | ✅ Read/Write | ❌ | ✅ Required |
| Side effects | Via | ✅ | ❌ |
| HTTP endpoints | Via ctx | ✅ | ❌ |
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
role: v.union(v.literal("admin"), v.literal("user")),
profileId: v.optional(v.id("profiles")),
})
.index("by_email", ["email"])
.index("by_role", ["role"]),
messages: defineTable({
authorId: v.id("users"),
body: v.string(),
channel: v.string(),
})
.index("by_channel", ["channel"])
.searchIndex("search_body", { searchField: "body" }),
});// convex/messages.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const list = query({
args: { channel: v.string(), limit: v.optional(v.number()) },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channel", args.channel))
.order("desc")
.take(args.limit ?? 50);
},
});// convex/messages.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const send = mutation({
args: { body: v.string(), channel: v.string() },
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Unauthorized");
return await ctx.db.insert("messages", {
authorId: identity.subject,
body: args.body,
channel: args.channel,
});
},
});// convex/ai.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { api, internal } from "./_generated/api";
export const generateResponse = action({
args: { prompt: v.string(), threadId: v.id("threads") },
handler: async (ctx, args) => {
// Call external API
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
body: JSON.stringify({ model: "gpt-4", messages: [{ role: "user", content: args.prompt }] }),
});
const data = await response.json();
// Write result via mutation
await ctx.runMutation(internal.messages.saveAIResponse, {
threadId: args.threadId,
content: data.choices[0].message.content,
});
},
});// app/page.tsx
"use client";
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
export default function Chat() {
const messages = useQuery(api.messages.list, { channel: "general" });
const sendMessage = useMutation(api.messages.send);
if (messages === undefined) return <div>Loading...</div>;
return (
<div>
{messages.map((msg) => <p key={msg._id}>{msg.body}</p>)}
<button onClick={() => sendMessage({ body: "Hello!", channel: "general" })}>
Send
</button>
</div>
);
}useQueryMath.random()Date.now()fetch_creationTimeDate.now()Doc<"tableName">Id<"tableName">| Validator | TypeScript Type | Example |
|---|---|---|
| | |
| | |
| | |
| | |
| | Document ID |
| | |
| | |
| | Optional field |
| | Union type |
| | Exact value |
| | Any value |
| | Binary data |
| | Key-value map |
Need to read data?
├─ Yes → query
│ └─ Need real-time updates? → useQuery (React)
│ └─ One-time fetch? → fetchQuery (Server)
└─ No
└─ Need to write data?
├─ Yes → mutation
│ └─ Also need external API? → mutation + scheduler.runAfter(0, action)
└─ No
└─ Need external API? → action
└─ Need durability? → Workflow component| Use Case | Function Type |
|---|---|
| Client can call | |
| Backend only | |
| Scheduled work | Internal functions |
| Security-sensitive | Internal functions |
my-app/
├── convex/
│ ├── _generated/ # Auto-generated (don't edit)
│ │ ├── api.d.ts
│ │ ├── api.js
│ │ ├── dataModel.d.ts
│ │ └── server.d.ts
│ ├── schema.ts # Database schema
│ ├── auth.ts # Auth configuration
│ ├── users.ts # User functions
│ ├── messages.ts # Message functions
│ ├── crons.ts # Scheduled jobs
│ ├── http.ts # HTTP endpoints
│ └── model/ # Business logic (recommended)
│ ├── users.ts
│ └── messages.ts
├── app/ # Next.js app
│ ├── ConvexClientProvider.tsx
│ └── page.tsx
└── .env.local
└── NEXT_PUBLIC_CONVEX_URL=...# Initialize Convex in project
npx convex init
# Start development server (watches for changes)
npx convex dev
# Deploy to production
npx convex deploy
# Open dashboard
npx convex dashboard
# Run a function manually
npx convex run messages:list '{"channel": "general"}'
# Import data
npx convex import --table messages data.json
# Export data
npx convex export --path ./backup# .env.local (development)
NEXT_PUBLIC_CONVEX_URL=https://your-project.convex.cloud
# Set in Convex dashboard for production
OPENAI_API_KEY=sk-...
CLERK_SECRET_KEY=sk_...const apiKey = process.env.OPENAI_API_KEY;