Convex Component Authoring
Create self-contained, reusable Convex components with proper isolation, exports, and dependency management for sharing across projects.
Documentation Sources
Before implementing, do not assume; fetch the latest documentation:
- Primary: https://docs.convex.dev/components
- Component Authoring: https://docs.convex.dev/components/authoring
- For broader context: https://docs.convex.dev/llms.txt
Instructions
What Are Convex Components?
Convex components are self-contained packages that include:
- Database tables (isolated from the main app)
- Functions (queries, mutations, actions)
- TypeScript types and validators
- Optional frontend hooks
Component Structure
my-convex-component/
├── package.json
├── tsconfig.json
├── README.md
├── src/
│ ├── index.ts # Main exports
│ ├── component.ts # Component definition
│ ├── schema.ts # Component schema
│ └── functions/
│ ├── queries.ts
│ ├── mutations.ts
│ └── actions.ts
└── convex.config.ts # Component configuration
Creating a Component
1. Component Configuration
typescript1// convex.config.ts 2import { defineComponent } from "convex/server"; 3 4export default defineComponent("myComponent");
2. Component Schema
typescript1// src/schema.ts 2import { defineSchema, defineTable } from "convex/server"; 3import { v } from "convex/values"; 4 5export default defineSchema({ 6 // Tables are isolated to this component 7 items: defineTable({ 8 name: v.string(), 9 data: v.any(), 10 createdAt: v.number(), 11 }).index("by_name", ["name"]), 12 13 config: defineTable({ 14 key: v.string(), 15 value: v.any(), 16 }).index("by_key", ["key"]), 17});
3. Component Definition
typescript1// src/component.ts 2import { defineComponent, ComponentDefinition } from "convex/server"; 3import schema from "./schema"; 4import * as queries from "./functions/queries"; 5import * as mutations from "./functions/mutations"; 6 7const component = defineComponent("myComponent", { 8 schema, 9 functions: { 10 ...queries, 11 ...mutations, 12 }, 13}); 14 15export default component;
4. Component Functions
typescript1// src/functions/queries.ts 2import { query } from "../_generated/server"; 3import { v } from "convex/values"; 4 5export const list = query({ 6 args: { 7 limit: v.optional(v.number()), 8 }, 9 returns: v.array(v.object({ 10 _id: v.id("items"), 11 name: v.string(), 12 data: v.any(), 13 createdAt: v.number(), 14 })), 15 handler: async (ctx, args) => { 16 return await ctx.db 17 .query("items") 18 .order("desc") 19 .take(args.limit ?? 10); 20 }, 21}); 22 23export const get = query({ 24 args: { name: v.string() }, 25 returns: v.union(v.object({ 26 _id: v.id("items"), 27 name: v.string(), 28 data: v.any(), 29 }), v.null()), 30 handler: async (ctx, args) => { 31 return await ctx.db 32 .query("items") 33 .withIndex("by_name", (q) => q.eq("name", args.name)) 34 .unique(); 35 }, 36});
typescript1// src/functions/mutations.ts 2import { mutation } from "../_generated/server"; 3import { v } from "convex/values"; 4 5export const create = mutation({ 6 args: { 7 name: v.string(), 8 data: v.any(), 9 }, 10 returns: v.id("items"), 11 handler: async (ctx, args) => { 12 return await ctx.db.insert("items", { 13 name: args.name, 14 data: args.data, 15 createdAt: Date.now(), 16 }); 17 }, 18}); 19 20export const update = mutation({ 21 args: { 22 id: v.id("items"), 23 data: v.any(), 24 }, 25 returns: v.null(), 26 handler: async (ctx, args) => { 27 await ctx.db.patch(args.id, { data: args.data }); 28 return null; 29 }, 30}); 31 32export const remove = mutation({ 33 args: { id: v.id("items") }, 34 returns: v.null(), 35 handler: async (ctx, args) => { 36 await ctx.db.delete(args.id); 37 return null; 38 }, 39});
5. Main Exports
typescript1// src/index.ts 2export { default as component } from "./component"; 3export * from "./functions/queries"; 4export * from "./functions/mutations"; 5 6// Export types for consumers 7export type { Id } from "./_generated/dataModel";
Using a Component
typescript1// In the consuming app's convex/convex.config.ts 2import { defineApp } from "convex/server"; 3import myComponent from "my-convex-component"; 4 5const app = defineApp(); 6 7app.use(myComponent, { name: "myComponent" }); 8 9export default app;
typescript1// In the consuming app's code 2import { useQuery, useMutation } from "convex/react"; 3import { api } from "../convex/_generated/api"; 4 5function MyApp() { 6 // Access component functions through the app's API 7 const items = useQuery(api.myComponent.list, { limit: 10 }); 8 const createItem = useMutation(api.myComponent.create); 9 10 return ( 11 <div> 12 {items?.map((item) => ( 13 <div key={item._id}>{item.name}</div> 14 ))} 15 <button onClick={() => createItem({ name: "New", data: {} })}> 16 Add Item 17 </button> 18 </div> 19 ); 20}
Component Configuration Options
typescript1// convex/convex.config.ts 2import { defineApp } from "convex/server"; 3import myComponent from "my-convex-component"; 4 5const app = defineApp(); 6 7// Basic usage 8app.use(myComponent); 9 10// With custom name 11app.use(myComponent, { name: "customName" }); 12 13// Multiple instances 14app.use(myComponent, { name: "instance1" }); 15app.use(myComponent, { name: "instance2" }); 16 17export default app;
Providing Component Hooks
typescript1// src/hooks.ts 2import { useQuery, useMutation } from "convex/react"; 3import { FunctionReference } from "convex/server"; 4 5// Type-safe hooks for component consumers 6export function useMyComponent(api: { 7 list: FunctionReference<"query">; 8 create: FunctionReference<"mutation">; 9}) { 10 const items = useQuery(api.list, {}); 11 const createItem = useMutation(api.create); 12 13 return { 14 items, 15 createItem, 16 isLoading: items === undefined, 17 }; 18}
Publishing a Component
package.json
json1{ 2 "name": "my-convex-component", 3 "version": "1.0.0", 4 "description": "A reusable Convex component", 5 "main": "dist/index.js", 6 "types": "dist/index.d.ts", 7 "files": [ 8 "dist", 9 "convex.config.ts" 10 ], 11 "scripts": { 12 "build": "tsc", 13 "prepublishOnly": "npm run build" 14 }, 15 "peerDependencies": { 16 "convex": "^1.0.0" 17 }, 18 "devDependencies": { 19 "convex": "^1.17.0", 20 "typescript": "^5.0.0" 21 }, 22 "keywords": [ 23 "convex", 24 "component" 25 ] 26}
tsconfig.json
json1{ 2 "compilerOptions": { 3 "target": "ES2020", 4 "module": "ESNext", 5 "moduleResolution": "bundler", 6 "declaration": true, 7 "outDir": "dist", 8 "strict": true, 9 "esModuleInterop": true, 10 "skipLibCheck": true 11 }, 12 "include": ["src/**/*"], 13 "exclude": ["node_modules", "dist"] 14}
Examples
Rate Limiter Component
typescript1// rate-limiter/src/schema.ts 2import { defineSchema, defineTable } from "convex/server"; 3import { v } from "convex/values"; 4 5export default defineSchema({ 6 requests: defineTable({ 7 key: v.string(), 8 timestamp: v.number(), 9 }) 10 .index("by_key", ["key"]) 11 .index("by_key_and_time", ["key", "timestamp"]), 12});
typescript1// rate-limiter/src/functions/mutations.ts 2import { mutation } from "../_generated/server"; 3import { v } from "convex/values"; 4 5export const checkLimit = mutation({ 6 args: { 7 key: v.string(), 8 limit: v.number(), 9 windowMs: v.number(), 10 }, 11 returns: v.object({ 12 allowed: v.boolean(), 13 remaining: v.number(), 14 resetAt: v.number(), 15 }), 16 handler: async (ctx, args) => { 17 const now = Date.now(); 18 const windowStart = now - args.windowMs; 19 20 // Clean old entries 21 const oldEntries = await ctx.db 22 .query("requests") 23 .withIndex("by_key_and_time", (q) => 24 q.eq("key", args.key).lt("timestamp", windowStart) 25 ) 26 .collect(); 27 28 for (const entry of oldEntries) { 29 await ctx.db.delete(entry._id); 30 } 31 32 // Count current window 33 const currentRequests = await ctx.db 34 .query("requests") 35 .withIndex("by_key", (q) => q.eq("key", args.key)) 36 .collect(); 37 38 const remaining = Math.max(0, args.limit - currentRequests.length); 39 const allowed = remaining > 0; 40 41 if (allowed) { 42 await ctx.db.insert("requests", { 43 key: args.key, 44 timestamp: now, 45 }); 46 } 47 48 const oldestRequest = currentRequests[0]; 49 const resetAt = oldestRequest 50 ? oldestRequest.timestamp + args.windowMs 51 : now + args.windowMs; 52 53 return { allowed, remaining: remaining - (allowed ? 1 : 0), resetAt }; 54 }, 55});
typescript1// Usage in consuming app 2import { useMutation } from "convex/react"; 3import { api } from "../convex/_generated/api"; 4 5function useRateLimitedAction() { 6 const checkLimit = useMutation(api.rateLimiter.checkLimit); 7 8 return async (action: () => Promise<void>) => { 9 const result = await checkLimit({ 10 key: "user-action", 11 limit: 10, 12 windowMs: 60000, 13 }); 14 15 if (!result.allowed) { 16 throw new Error(`Rate limited. Try again at ${new Date(result.resetAt)}`); 17 } 18 19 await action(); 20 }; 21}
Best Practices
- Never run
npx convex deployunless explicitly instructed - Never run any git commands unless explicitly instructed
- Keep component tables isolated (don't reference main app tables)
- Export clear TypeScript types for consumers
- Document all public functions and their arguments
- Use semantic versioning for component releases
- Include comprehensive README with examples
- Test components in isolation before publishing
Common Pitfalls
- Cross-referencing tables - Component tables should be self-contained
- Missing type exports - Export all necessary types
- Hardcoded configuration - Use component options for customization
- No versioning - Follow semantic versioning
- Poor documentation - Document all public APIs
References
- Convex Documentation: https://docs.convex.dev/
- Convex LLMs.txt: https://docs.convex.dev/llms.txt
- Components: https://docs.convex.dev/components
- Component Authoring: https://docs.convex.dev/components/authoring