← Back to list

convex-react-integration
by AmirZhou
⭐ 0🍴 0📅 Jan 17, 2026
SKILL.md
name: convex-react-integration description: Complete Convex backend integration patterns for React applications with Clerk authentication. Use this skill when setting up Convex with React, implementing CRUD operations, file uploads, rate limiting, or external API integrations via actions. Triggers on requests for real-time databases, Convex setup, or serverless backend with React.
Convex React Integration
Production-ready patterns for integrating Convex with React applications, including Clerk auth, schema design, and advanced features.
Setup
Provider Configuration
// src/components/providers.tsx
import { ConvexProviderWithClerk } from "convex/react-clerk";
import { ClerkProvider, useAuth } from "@clerk/clerk-react";
import { ConvexReactClient } from "convex/react";
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL);
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
{children}
</ConvexProviderWithClerk>
</ClerkProvider>
);
}
Auth Config
// convex/auth.config.ts
export default {
providers: [
{
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
applicationID: "convex",
},
],
};
Authentication Helper
// convex/lib/auth.ts
import { QueryCtx, MutationCtx } from "./_generated/server";
export async function getAuthUserId(ctx: QueryCtx | MutationCtx): Promise<string> {
const identity = await ctx.auth.getUserIdentity();
if (!identity) {
throw new Error("Not authenticated. Please sign in.");
}
return identity.subject;
}
// For queries that should return null instead of throwing
export async function getOptionalUserId(ctx: QueryCtx): Promise<string | null> {
const identity = await ctx.auth.getUserIdentity();
return identity?.subject ?? null;
}
Schema Design
User-Owned Table Pattern
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
projects: defineTable({
userId: v.string(),
name: v.string(),
description: v.optional(v.string()),
status: v.union(v.literal("active"), v.literal("archived")),
createdAt: v.number(),
updatedAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_user_status", ["userId", "status"]),
items: defineTable({
userId: v.string(),
projectId: v.id("projects"),
title: v.string(),
content: v.optional(v.string()),
order: v.number(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_project", ["projectId"]),
});
Indexing Strategy
- Single-field indexes for common filters:
by_user,by_project - Composite indexes for combined filters:
by_user_status - Always include
userIdindex on user-owned tables
CRUD Patterns
Query with Ownership
// convex/projects.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { getAuthUserId } from "./lib/auth";
export const list = query({
args: {
status: v.optional(v.union(v.literal("active"), v.literal("archived"))),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (args.status) {
return ctx.db
.query("projects")
.withIndex("by_user_status", (q) =>
q.eq("userId", userId).eq("status", args.status!)
)
.collect();
}
return ctx.db
.query("projects")
.withIndex("by_user", (q) => q.eq("userId", userId))
.collect();
},
});
export const get = query({
args: { id: v.id("projects") },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
const project = await ctx.db.get(args.id);
if (!project || project.userId !== userId) {
return null;
}
return project;
},
});
Mutation with Ownership Verification
export const create = mutation({
args: {
name: v.string(),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
const now = Date.now();
return ctx.db.insert("projects", {
userId,
name: args.name,
description: args.description,
status: "active",
createdAt: now,
updatedAt: now,
});
},
});
export const update = mutation({
args: {
id: v.id("projects"),
name: v.optional(v.string()),
description: v.optional(v.string()),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
const existing = await ctx.db.get(args.id);
if (!existing || existing.userId !== userId) {
throw new Error("Project not found or not authorized");
}
const updates: Partial<typeof existing> = { updatedAt: Date.now() };
if (args.name !== undefined) updates.name = args.name;
if (args.description !== undefined) updates.description = args.description;
await ctx.db.patch(args.id, updates);
},
});
export const remove = mutation({
args: { id: v.id("projects") },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
const existing = await ctx.db.get(args.id);
if (!existing || existing.userId !== userId) {
throw new Error("Project not found or not authorized");
}
// Cascade: unlink related items
const items = await ctx.db
.query("items")
.withIndex("by_project", (q) => q.eq("projectId", args.id))
.collect();
for (const item of items) {
await ctx.db.delete(item._id);
}
await ctx.db.delete(args.id);
},
});
Frontend Usage
Queries and Mutations
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
function ProjectList() {
// Query with conditional execution
const projects = useQuery(api.projects.list, { status: "active" });
// Skip query until ready
const projectId = selectedId;
const project = useQuery(
api.projects.get,
projectId ? { id: projectId } : "skip"
);
// Mutations
const createProject = useMutation(api.projects.create);
const updateProject = useMutation(api.projects.update);
const deleteProject = useMutation(api.projects.remove);
const handleCreate = async () => {
const id = await createProject({ name: "New Project" });
console.log("Created:", id);
};
if (projects === undefined) return <Loading />;
return (
<ul>
{projects.map((p) => (
<li key={p._id}>{p.name}</li>
))}
</ul>
);
}
File Upload
Generate Upload URL
// convex/files.ts
export const generateUploadUrl = mutation({
handler: async (ctx) => {
await getAuthUserId(ctx); // Ensure authenticated
return ctx.storage.generateUploadUrl();
},
});
export const saveFile = mutation({
args: {
storageId: v.id("_storage"),
projectId: v.id("projects"),
filename: v.string(),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
const url = await ctx.storage.getUrl(args.storageId);
return ctx.db.insert("files", {
userId,
projectId: args.projectId,
storageId: args.storageId,
filename: args.filename,
url,
createdAt: Date.now(),
});
},
});
Frontend Upload
function FileUploader({ projectId }: { projectId: Id<"projects"> }) {
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);
const handleUpload = async (file: File) => {
// Step 1: Get upload URL
const uploadUrl = await generateUploadUrl();
// Step 2: Upload file directly to Convex storage
const result = await fetch(uploadUrl, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { storageId } = await result.json();
// Step 3: Save file record
await saveFile({
storageId,
projectId,
filename: file.name,
});
};
return (
<input
type="file"
onChange={(e) => e.target.files?.[0] && handleUpload(e.target.files[0])}
/>
);
}
Actions (External APIs)
// convex/ai.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { api, internal } from "./_generated/api";
export const generateContent = action({
args: {
prompt: v.string(),
projectId: v.id("projects"),
},
handler: async (ctx, args) => {
// 1. Authenticate
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const userId = identity.subject;
// 2. Get data via internal query
const project = await ctx.runQuery(api.projects.get, { id: args.projectId });
if (!project) throw new Error("Project not found");
// 3. Call external API
const response = await fetch("https://api.example.com/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.API_KEY}`,
},
body: JSON.stringify({ prompt: args.prompt }),
});
const data = await response.json();
// 4. Save result via internal mutation
await ctx.runMutation(internal.ai.saveResult, {
projectId: args.projectId,
content: data.content,
userId,
});
return { success: true, content: data.content };
},
});
Rate Limiting
// convex/rateLimits.ts
import { RateLimiter, MINUTE, HOUR } from "@convex-dev/rate-limiter";
import { components } from "./_generated/api";
export const rateLimiter = new RateLimiter(components.rateLimiter, {
createProject: { kind: "token bucket", rate: 10, period: MINUTE, capacity: 5 },
aiGeneration: { kind: "fixed window", rate: 10, period: HOUR },
});
// Usage in mutation
export const create = mutation({
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
await rateLimiter.limit(ctx, "createProject", { key: userId, throws: true });
// ... rest of mutation
},
});
Resources
- See
references/schema-patterns.mdfor advanced schema designs - See
references/file-upload.mdfor complex file processing workflows
Score
Total Score
50/100
Based on repository quality metrics
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
Reviews
💬
Reviews coming soon