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
- Component Configuration
// convex.config.ts import { defineComponent } from "convex/server";
export default defineComponent("myComponent");
- Component Schema
// src/schema.ts import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values";
export default defineSchema({ // Tables are isolated to this component items: defineTable({ name: v.string(), data: v.any(), createdAt: v.number(), }).index("by_name", ["name"]),
config: defineTable({ key: v.string(), value: v.any(), }).index("by_key", ["key"]), });
- Component Definition
// src/component.ts import { defineComponent, ComponentDefinition } from "convex/server"; import schema from "./schema"; import * as queries from "./functions/queries"; import * as mutations from "./functions/mutations";
const component = defineComponent("myComponent", { schema, functions: { ...queries, ...mutations, }, });
export default component;
- Component Functions
// src/functions/queries.ts import { query } from "../_generated/server"; import { v } from "convex/values";
export const list = query({ args: { limit: v.optional(v.number()), }, returns: v.array(v.object({ _id: v.id("items"), name: v.string(), data: v.any(), createdAt: v.number(), })), handler: async (ctx, args) => { return await ctx.db .query("items") .order("desc") .take(args.limit ?? 10); }, });
export const get = query({ args: { name: v.string() }, returns: v.union(v.object({ _id: v.id("items"), name: v.string(), data: v.any(), }), v.null()), handler: async (ctx, args) => { return await ctx.db .query("items") .withIndex("by_name", (q) => q.eq("name", args.name)) .unique(); }, });
// src/functions/mutations.ts import { mutation } from "../_generated/server"; import { v } from "convex/values";
export const create = mutation({ args: { name: v.string(), data: v.any(), }, returns: v.id("items"), handler: async (ctx, args) => { return await ctx.db.insert("items", { name: args.name, data: args.data, createdAt: Date.now(), }); }, });
export const update = mutation({ args: { id: v.id("items"), data: v.any(), }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.patch(args.id, { data: args.data }); return null; }, });
export const remove = mutation({ args: { id: v.id("items") }, returns: v.null(), handler: async (ctx, args) => { await ctx.db.delete(args.id); return null; }, });
- Main Exports
// src/index.ts export { default as component } from "./component"; export * from "./functions/queries"; export * from "./functions/mutations";
// Export types for consumers export type { Id } from "./_generated/dataModel";
Using a Component
// In the consuming app's convex/convex.config.ts import { defineApp } from "convex/server"; import myComponent from "my-convex-component";
const app = defineApp();
app.use(myComponent, { name: "myComponent" });
export default app;
// In the consuming app's code import { useQuery, useMutation } from "convex/react"; import { api } from "../convex/_generated/api";
function MyApp() { // Access component functions through the app's API const items = useQuery(api.myComponent.list, { limit: 10 }); const createItem = useMutation(api.myComponent.create);
return ( <div> {items?.map((item) => ( <div key={item._id}>{item.name}</div> ))} <button onClick={() => createItem({ name: "New", data: {} })}> Add Item </button> </div> ); }
Component Configuration Options
// convex/convex.config.ts import { defineApp } from "convex/server"; import myComponent from "my-convex-component";
const app = defineApp();
// Basic usage app.use(myComponent);
// With custom name app.use(myComponent, { name: "customName" });
// Multiple instances app.use(myComponent, { name: "instance1" }); app.use(myComponent, { name: "instance2" });
export default app;
Providing Component Hooks
// src/hooks.ts import { useQuery, useMutation } from "convex/react"; import { FunctionReference } from "convex/server";
// Type-safe hooks for component consumers export function useMyComponent(api: { list: FunctionReference<"query">; create: FunctionReference<"mutation">; }) { const items = useQuery(api.list, {}); const createItem = useMutation(api.create);
return { items, createItem, isLoading: items === undefined, }; }
Publishing a Component
package.json
{ "name": "my-convex-component", "version": "1.0.0", "description": "A reusable Convex component", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ "dist", "convex.config.ts" ], "scripts": { "build": "tsc", "prepublishOnly": "npm run build" }, "peerDependencies": { "convex": "^1.0.0" }, "devDependencies": { "convex": "^1.17.0", "typescript": "^5.0.0" }, "keywords": [ "convex", "component" ] }
tsconfig.json
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", "declaration": true, "outDir": "dist", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
Examples
Rate Limiter Component
// rate-limiter/src/schema.ts import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values";
export default defineSchema({ requests: defineTable({ key: v.string(), timestamp: v.number(), }) .index("by_key", ["key"]) .index("by_key_and_time", ["key", "timestamp"]), });
// rate-limiter/src/functions/mutations.ts import { mutation } from "../_generated/server"; import { v } from "convex/values";
export const checkLimit = mutation({ args: { key: v.string(), limit: v.number(), windowMs: v.number(), }, returns: v.object({ allowed: v.boolean(), remaining: v.number(), resetAt: v.number(), }), handler: async (ctx, args) => { const now = Date.now(); const windowStart = now - args.windowMs;
// Clean old entries
const oldEntries = await ctx.db
.query("requests")
.withIndex("by_key_and_time", (q) =>
q.eq("key", args.key).lt("timestamp", windowStart)
)
.collect();
for (const entry of oldEntries) {
await ctx.db.delete(entry._id);
}
// Count current window
const currentRequests = await ctx.db
.query("requests")
.withIndex("by_key", (q) => q.eq("key", args.key))
.collect();
const remaining = Math.max(0, args.limit - currentRequests.length);
const allowed = remaining > 0;
if (allowed) {
await ctx.db.insert("requests", {
key: args.key,
timestamp: now,
});
}
const oldestRequest = currentRequests[0];
const resetAt = oldestRequest
? oldestRequest.timestamp + args.windowMs
: now + args.windowMs;
return { allowed, remaining: remaining - (allowed ? 1 : 0), resetAt };
}, });
// Usage in consuming app import { useMutation } from "convex/react"; import { api } from "../convex/_generated/api";
function useRateLimitedAction() { const checkLimit = useMutation(api.rateLimiter.checkLimit);
return async (action: () => Promise<void>) => { const result = await checkLimit({ key: "user-action", limit: 10, windowMs: 60000, });
if (!result.allowed) {
throw new Error(`Rate limited. Try again at ${new Date(result.resetAt)}`);
}
await action();
}; }
Best Practices
-
Never run npx convex deploy unless 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