Loading...
Loading...
Builds reusable Convex components with isolated tables and app-facing APIs. Use for new components, reusable backend modules, integrations, or component boundary work.
npx skill4agent add convex/agent-skills convex-create-componentconvex/convex.config.tsschema.ts./_generated/serverapp.use(...)convex/convex.config.tscomponents.<name>ctx.runQueryctx.runMutationctx.runActionnpx convex dev| Goal | Shape | Reference |
|---|---|---|
| Component for this app only | Local | |
| Publish or share across apps | Packaged | |
| User explicitly needs local + shared library code | Hybrid | |
| Not sure | Default to local | |
convex/components/<componentName>/defineComponent(...)convex.config.tsconvex/convex.config.tsapp.use(...)npx convex dev_generated/// convex/components/notifications/convex.config.ts
import { defineComponent } from "convex/server";
export default defineComponent("notifications");// convex/components/notifications/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
notifications: defineTable({
userId: v.string(),
message: v.string(),
read: v.boolean(),
}).index("by_user_read", ["userId", "read"]),
});// convex/components/notifications/lib.ts
import { v } from "convex/values";
import { mutation, query } from "./_generated/server.js";
export const send = mutation({
args: { userId: v.string(), message: v.string() },
returns: v.id("notifications"),
handler: async (ctx, args) => {
return await ctx.db.insert("notifications", {
userId: args.userId,
message: args.message,
read: false,
});
},
});
export const listUnread = query({
args: { userId: v.string() },
returns: v.array(
v.object({
_id: v.id("notifications"),
_creationTime: v.number(),
userId: v.string(),
message: v.string(),
read: v.boolean(),
}),
),
handler: async (ctx, args) => {
return await ctx.db
.query("notifications")
.withIndex("by_user_read", (q) =>
q.eq("userId", args.userId).eq("read", false),
)
.collect();
},
});// convex/convex.config.ts
import { defineApp } from "convex/server";
import notifications from "./components/notifications/convex.config.js";
const app = defineApp();
app.use(notifications);
export default app;// convex/notifications.ts (app-side wrapper)
import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { getAuthUserId } from "@convex-dev/auth/server";
export const sendNotification = mutation({
args: { message: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
await ctx.runMutation(components.notifications.lib.send, {
userId,
message: args.message,
});
return null;
},
});
export const myUnread = query({
args: {},
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
return await ctx.runQuery(components.notifications.lib.listUnread, {
userId,
});
},
});convex/components/notifications/lib.tscomponents.notifications.lib.sendctx.authprocess.envIdComponentApiv.id("parentTable")querymutationaction./_generated/serverconvex/http.tspaginatorconvex-helpers.paginate().paginate().filter()argsreturns// Bad: component code cannot rely on app auth or env
const identity = await ctx.auth.getUserIdentity();
const apiKey = process.env.OPENAI_API_KEY;// Good: the app resolves auth and env, then passes explicit values
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
await ctx.runAction(components.translator.translate, {
userId,
apiKey: process.env.OPENAI_API_KEY,
text: args.text,
});// Bad: assuming a component function is directly callable by clients
export const send = components.notifications.send;// Good: re-export through an app mutation or query
export const sendNotification = mutation({
args: { message: v.string() },
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Not authenticated");
await ctx.runMutation(components.notifications.lib.send, {
userId,
message: args.message,
});
return null;
},
});// Bad: parent app table IDs are not valid component validators
args: {
userId: v.id("users"),
}// Good: treat parent-owned IDs as strings at the boundary
args: {
userId: v.string(),
}references/advanced-patterns.mdnpx convex codegen --component-dir convex/components/<name>npx convex codegennpx convex devCONVEX_DEPLOYMENT./_generated/*components.<name>...references/local-components.mdreferences/packaged-components.mdreferences/hybrid-components.mdconvex/components/<name>/./_generated/serverv.string()argsreturnsnpx convex dev