<< All versions
Skill v1.0.0
currentAutomated scan100/100majiayu000/claude-skill-registry/convex-component-authoring-majiayu000-claude-skill-registr
──Details
PublishedMarch 19, 2026 at 01:34 AM
Content Hashsha256:561346aded096963...
Git SHAdiscovery:37
──Files
Files (1 file, 10.3 KB)
SKILL.md10.3 KBactive
SKILL.md · 458 lines · 10.3 KB
name: convex-component-authoring displayName: Convex Component Authoring description: How to create, structure, and publish self-contained Convex components with proper isolation, exports, and dependency management version: 1.0.0 author: Convex tags: [convex, components, reusable, packages, npm]
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
typescript
// convex.config.tsimport { defineComponent } from "convex/server";export default defineComponent("myComponent");
2. Component Schema
typescript
// src/schema.tsimport { defineSchema, defineTable } from "convex/server";import { v } from "convex/values";export default defineSchema({// Tables are isolated to this componentitems: 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"]),});
3. Component Definition
typescript
// src/component.tsimport { 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;
4. Component Functions
typescript
// src/functions/queries.tsimport { 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();},});
typescript
// src/functions/mutations.tsimport { 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;},});
5. Main Exports
typescript
// src/index.tsexport { default as component } from "./component";export * from "./functions/queries";export * from "./functions/mutations";// Export types for consumersexport type { Id } from "./_generated/dataModel";
Using a Component
typescript
// In the consuming app's convex/convex.config.tsimport { defineApp } from "convex/server";import myComponent from "my-convex-component";const app = defineApp();app.use(myComponent, { name: "myComponent" });export default app;
typescript
// In the consuming app's codeimport { useQuery, useMutation } from "convex/react";import { api } from "../convex/_generated/api";function MyApp() {// Access component functions through the app's APIconst 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
typescript
// convex/convex.config.tsimport { defineApp } from "convex/server";import myComponent from "my-convex-component";const app = defineApp();// Basic usageapp.use(myComponent);// With custom nameapp.use(myComponent, { name: "customName" });// Multiple instancesapp.use(myComponent, { name: "instance1" });app.use(myComponent, { name: "instance2" });export default app;
Providing Component Hooks
typescript
// src/hooks.tsimport { useQuery, useMutation } from "convex/react";import { FunctionReference } from "convex/server";// Type-safe hooks for component consumersexport 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
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
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
typescript
// rate-limiter/src/schema.tsimport { 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"]),});
typescript
// rate-limiter/src/functions/mutations.tsimport { 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 entriesconst 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 windowconst 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 };},});
typescript
// Usage in consuming appimport { 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 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