avoid-feature-creep
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use when extracting a feature into a package, building something for the Convex component directory, or when a component's functions are not showing up in
$ npx -y skills add waynesutton/builder-skills --skill convex-component-authoring --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/convex-component-authoringContext preview
The summary Claude sees to decide when to auto-load this skill.
Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use when extracting a feature into a package, building something for the Convex component directory, or when a component's functions are not showing up in
name: convex-component-authoring description: Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use when extracting a feature into a package, building something for the Convex component directory, or when a component's functions are not showing up in the parent app.
Produces a self contained component: its own `convex.config.ts`, schema, functions, and a typed client wrapper the parent app calls. The one rule: a component cannot read the parent's tables, `ctx.auth`, or `process.env`. Whatever it needs, the app passes in as arguments.
| Need | Use | | --- | --- | | Shared logic over the app's own tables | Plain TypeScript helper in `convex/lib/` | | Its own tables with a schema the app cannot touch | Component | | Isolated functions that run in their own transaction | Component | | Reuse across apps with one install line | Component | | A React hook or client utility only | npm library, no component |
If the feature does not need persistent state behind an API boundary, a helper function is less work and easier to type.
my-component/
package.json
src/
component/
convex.config.ts # defineComponent("counter")
schema.ts # tables only this component can see
public.ts # queries, mutations, actions
_generated/ # created by npx convex codegen
client/
index.ts # wrapper class the app imports
example/
convex/
convex.config.ts # app.use(counter) for local testingFor a local only component, put the `component/` contents at `convex/components/<name>/` and skip `client/` and `package.json`.
// src/component/convex.config.ts
import { defineComponent } from "convex/server";
const component = defineComponent("counter");
export default component;// src/component/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
counters: defineTable({
name: v.string(),
value: v.number(),
}).index("by_name", ["name"]),
});Functions import `query` and `mutation` from the component's own `_generated/server`, never from the app's. Every public function needs `args` and `returns` validators; without them the parent sees `any`.
// src/component/public.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";
export const add = mutation({
args: { name: v.string(), amount: v.number() },
returns: v.number(),
handler: async (ctx, args) => {
const existing = await ctx.db
.query("counters")
.withIndex("by_name", (q) => q.eq("name", args.name))
.unique();
if (!existing) {
await ctx.db.insert("counters", { name: args.name, value: args.amount });
return args.amount;
}
const value = existing.value + args.amount;
await ctx.db.patch(existing._id, { value });
return value;
},
});
export const get = query({
args: { name: v.string() },
returns: v.number(),
handler: async (ctx, args) => {
const counter = await ctx.db
.query("counters")
.withIndex("by_name", (q) => q.eq("name", args.name))
.unique();
return counter?.value ?? 0;
},
});// convex/convex.config.ts (parent app)
import { defineApp } from "convex/server";
import counter from "@acme/counter/convex.config.js";
// local component: import counter from "./components/counter/convex.config.js";
const app = defineApp();
app.use(counter);
// A second instance under a different name:
// app.use(counter, { name: "pageViews" });
export default app;Run `npx convex dev`. It generates `components.counter` in the app's `convex/_generated/api` and the component's own `_generated/` folder. The reference path mirrors the file: a function in `public.ts` is `components.counter.public.add`.
Component functions arrive in the parent as internal references. Call them with `ctx.runQuery` and `ctx.runMutation`; clients cannot hit them directly.
The wrapper runs in the app's environment, so it can read `ctx.auth` and `process.env` and pass what the component needs. It takes the component reference first and options second.
// src/client/index.ts
import type {
GenericDataModel,
GenericMutationCtx,
GenericQueryCtx,
} from "convex/server";
import type { ComponentApi } from "../component/_generated/component.js";
// Pick only the capabilities each method needs so any ctx with runQuery works
type QueryCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;
export class Counter {
constructor(
public component: ComponentApi,
private options: { prefix?: string } = {},
) {}
private key(name: string): string {
return this.options.prefix ? `${this.options.prefix}:${name}` : name;
}
async add(ctx: MutationCtx, name: string, amount = 1): Promise<number> {
return await ctx.runMutation(this.component.public.add, {
name: this.key(name),
amount,
});
}
async get(ctx: QueryCtx, name: string): Promise<number> {
return await ctx.runQuery(this.component.public.get, { name: this.key(name) });
}
}// convex/counter.ts (parent app)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";Agent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with…
Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent…
Schedules work in Convex: cron jobs in convex/crons.ts, one off scheduled functions with runAfter and runAt, batching large jobs,…
Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the…
Writes Convex queries, mutations, actions, and internal functions in the object form with args and returns validators, correct…