convex-component-authoring

Self-contained Convex packages with isolated databases, functions, and type-safe exports for reuse across projects. Includes isolated database tables, queries, mutations, actions, and TypeScript types within a single component package Supports multiple component instances in a single app with custom naming and configuration options Provides optional React hooks for type-safe frontend integration and simplified consumer APIs Publishes to npm with proper peer dependencies, TypeScript declarations, and semantic versioning

INSTALLATION
npx skills add https://github.com/waynesutton/convexskills --skill convex-component-authoring
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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:

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

// convex.config.ts

import { defineComponent } from "convex/server";

export default defineComponent("myComponent");

#### 2. 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"]),

});

#### 3. 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;

#### 4. 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;

  },

});

#### 5. 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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card