Claude Agent Skill · by Waynesutton

Convex Component Authoring

Convex Component Authoring teaches developers how to create self-contained, reusable Convex components with isolated database tables, functions, and TypeScript

convexcomponentsreusablepackagesnpm
Install
Terminal · npx
$npx skills add https://github.com/waynesutton/convexskills --skill convex-component-authoring
Works with Paperclip

How Convex Component Authoring fits into a Paperclip company.

Convex Component Authoring drops into any Paperclip agent that handles convex and components work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md457 lines
Expand
---name: convex-component-authoringdisplayName: Convex Component Authoringdescription: How to create, structure, and publish self-contained Convex components with proper isolation, exports, and dependency managementversion: 1.0.0author: Convextags: [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 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"]),});``` #### 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 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 ```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 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 };  },});``` ```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 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 1. **Cross-referencing tables** - Component tables should be self-contained2. **Missing type exports** - Export all necessary types3. **Hardcoded configuration** - Use component options for customization4. **No versioning** - Follow semantic versioning5. **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