How to create, structure, and publish self-contained Convex components with proper isolation, exports, and dependency management
59
48%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Optimize this skill with Tessl
npx tessl skill review --optimize ./skills/convex-component-authoring/SKILL.mdCreate self-contained, reusable Convex components with proper isolation, exports, and dependency management for sharing across projects.
Before implementing, do not assume; fetch the latest documentation:
Convex components are self-contained packages that include:
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// convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("myComponent");// 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"]),
});// 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;// 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;
},
});// 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";// 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>
);
}// 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;// 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,
};
}{
"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"
]
}{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}// 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();
};
}npx convex deploy unless explicitly instructed8ef49c9
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.