---
id: sm3-convex-create-component
name: "convex-create-component"
url: https://skills.yangsir.net/skill/sm3-convex-create-component
author: get-convex
domain: ai-backend-engineering
tags: ["convex-development", "component-creation", "backend-engineering", "reusable-modules"]
install_count: 51200
rating: 4.70 (608 reviews)
github: https://github.com/get-convex/agent-skills
---

# convex-create-component

> 创建具有清晰边界和可重用性的Convex组件，简化后端开发，提高代码复用性和项目可维护性。

**Stats**: 51,200 installs · 4.7/5 (608 reviews)

## Before / After 对比

### Convex组件创建

## Readme

# convex-create-component

# Convex Create Component

Create reusable Convex components with clear boundaries and a small app-facing API.

## When to Use

- Creating a new Convex component in an existing app

- Extracting reusable backend logic into a component

- Building a third-party integration that should own its own tables and workflows

- Packaging Convex functionality for reuse across multiple apps

## When Not to Use

- One-off business logic that belongs in the main app

- Thin utilities that do not need Convex tables or functions

- App-level orchestration that should stay in `convex/`

- Cases where a normal TypeScript library is enough

## Workflow

- Ask the user what they are building and what the end goal is. If the repo already makes the answer obvious, say so and confirm before proceeding.

- Choose the shape using the decision tree below and read the matching reference file.

- Decide whether a component is justified. Prefer normal app code or a regular library if the feature does not need isolated tables, backend functions, or reusable persistent state.

- Make a short plan for:

what tables the component owns

- what public functions it exposes

- what data must be passed in from the app (auth, env vars, parent IDs)

- what stays in the app as wrappers or HTTP mounts

- Create the component structure with `convex.config.ts`, `schema.ts`, and function files.

- Implement functions using the component's own `./_generated/server` imports, not the app's generated files.

- Wire the component into the app with `app.use(...)`. If the app does not already have `convex/convex.config.ts`, create it.

- Call the component from the app through `components.<name>` using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction`.

- If React clients, HTTP callers, or public APIs need access, create wrapper functions in the app instead of exposing component functions directly.

- Run `npx convex dev` and fix codegen, type, or boundary issues before finishing.

## Choose the Shape

Ask the user, then pick one path:

Goal
Shape
Reference

Component for this app only
Local
`references/local-components.md`

Publish or share across apps
Packaged
`references/packaged-components.md`

User explicitly needs local + shared library code
Hybrid
`references/hybrid-components.md`

Not sure
Default to local
`references/local-components.md`

Read exactly one reference file before proceeding.

## Default Approach

Unless the user explicitly wants an npm package, default to a local component:

- Put it under `convex/components/<componentName>/`

- Define it with `defineComponent(...)` in its own `convex.config.ts`

- Install it from the app's `convex/convex.config.ts` with `app.use(...)`

- Let `npx convex dev` generate the component's own `_generated/` files

## Component Skeleton

A minimal local component with a table and two functions, plus the app wiring.

```
// 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", ["userId"]),
});

```

```
// 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", (q) => q.eq("userId", args.userId))
      .filter((q) => q.eq(q.field("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,
    });
  },
});

```

Note the reference path shape: a function in `convex/components/notifications/lib.ts` is called as `components.notifications.lib.send` from the app.

## Critical Rules

- Keep authentication in the app. `ctx.auth` is not available inside components.

- Keep environment access in the app. Component functions cannot read `process.env`.

- Pass parent app IDs across the boundary as strings. `Id` types become plain strings in the app-facing `ComponentApi`.

- Do not use `v.id("parentTable")` for app-owned tables inside component args or schema.

- Import `query`, `mutation`, and `action` from the component's own `./_generated/server`.

- Do not expose component functions directly to clients. Create app wrappers when client access is needed.

- If the component defines HTTP handlers, mount the routes in the app's `convex/http.ts`.

- If the component needs pagination, use `paginator` from `convex-helpers` instead of built-in `.paginate()`.

- Add `args` and `returns` validators to all public component functions.

## Patterns

### Authentication and environment access

```
// 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,
});

```

### Client-facing API

```
// 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;
  },
});

```

### IDs across the boundary

```
// 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() }

```

### Function Handles for callbacks

When the app needs to pass a callback function to the component, use function handles. This is common for components that run app-defined logic on a schedule or in a workflow.

```
// App side: create a handle and pass it to the component
import { createFunctionHandle } from "convex/server";

export const startJob = mutation({
  handler: async (ctx) => {
    const handle = await createFunctionHandle(internal.myModule.processItem);
    await ctx.runMutation(components.workpool.enqueue, {
      callback: handle,
    });
  },
});

```

```
// Component side: accept and invoke the handle
import { v } from "convex/values";
import type { FunctionHandle } from "convex/server";
import { mutation } from "./_generated/server.js";

export const enqueue = mutation({
  args: { callback: v.string() },
  handler: async (ctx, args) => {
    const handle = args.callback as FunctionHandle<"mutation">;
    await ctx.scheduler.runAfter(0, handle, {});
  },
});

```

### Deriving validators from schema

Instead of manually repeating field types in return validators, extend the schema validator:

```
import { v } from "convex/values";
import schema from "./schema.js";

const notificationDoc = schema.tables.notifications.validator.extend({
  _id: v.id("notifications"),
  _creationTime: v.number(),
});

export const getLatest = query({
  args: {},
  returns: v.nullable(notificationDoc),
  handler: async (ctx) => {
    return await ctx.db.query("notifications").order("desc").first();
  },
});

```

### Static configuration with a globals table

A common pattern for component configuration is a single-document "globals" table:

```
// schema.ts
export default defineSchema({
  globals: defineTable({
    maxRetries: v.number(),
    webhookUrl: v.optional(v.string()),
  }),
  // ... other tables
});

```

```
// lib.ts
export const configure = mutation({
  args: { maxRetries: v.number(), webhookUrl: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, args) => {
    const existing = await ctx.db.query("globals").first();
    if (existing) {
      await ctx.db.patch(existing._id, args);
    } else {
      await ctx.db.insert("globals", args);
    }
    return null;
  },
});

```

### Class-based client wrappers

For components with many functions or configuration options, a class-based client provides a cleaner API. This pattern is common in published components.

```
// src/client/index.ts
import type { GenericMutationCtx, GenericDataModel } from "convex/server";
import type { ComponentApi } from "../component/_generated/component.js";

type MutationCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;

export class Notifications {
  constructor(
    private component: ComponentApi,
    private options?: { defaultChannel?: string },
  ) {}

  async send(ctx: MutationCtx, args: { userId: string; message: string }) {
    return await ctx.runMutation(this.component.lib.send, {
      ...args,
      channel: this.options?.defaultChannel ?? "default",
    });
  }
}

```

```
// App usage
import { Notifications } from "@convex-dev/notifications";
import { components } from "./_generated/api";

const notifications = new Notifications(components.notifications, {
  defaultChannel: "alerts",
});

export const send = mutation({
  args: { message: v.string() },
  handler: async (ctx, args) => {
    const userId = await getAuthUserId(ctx);
    await notifications.send(ctx, { userId, message: args.message });
  },
});

```

## Validation

Try validation in this order:

- `npx convex codegen --component-dir convex/components/<name>`

- `npx convex codegen`

- `npx convex dev`

Important:

- Fresh repos may fail these commands until `CONVEX_DEPLOYMENT` is configured.

- Until codegen runs, component-local `./_generated/*` imports and app-side `components.<name>...` references will not typecheck.

- If validation blocks on Convex login or deployment setup, stop and ask the user for that exact step instead of guessing.

## Reference Files

Read exactly one of these after the user confirms the goal:

- `references/local-components.md`

- `references/packaged-components.md`

- `references/hybrid-components.md`

Official docs: [Authoring Components](https://docs.convex.dev/components/authoring)

## Checklist

-  Asked the user what they want to build and confirmed the shape

-  Read the matching reference file

-  Confirmed a component is the right abstraction

-  Planned tables, public API, boundaries, and app wrappers

-  Component lives under `convex/components/<name>/` (or package layout if publishing)

-  Component imports from its own `./_generated/server`

-  Auth, env access, and HTTP routes stay in the app

-  Parent app IDs cross the boundary as `v.string()`

-  Public functions have `args` and `returns` validators

-  Ran `npx convex dev` and fixed codegen or type issues

Weekly Installs524Repository[get-convex/agent-skills](https://github.com/get-convex/agent-skills)GitHub Stars9First Seen2 days agoSecurity Audits[Gen Agent Trust HubPass](/get-convex/agent-skills/convex-create-component/security/agent-trust-hub)[SocketPass](/get-convex/agent-skills/convex-create-component/security/socket)[SnykPass](/get-convex/agent-skills/convex-create-component/security/snyk)Installed onopencode523gemini-cli523github-copilot523amp523cline523codex523

---
*Source: https://skills.yangsir.net/skill/sm3-convex-create-component*
*Markdown mirror: https://skills.yangsir.net/api/skill/sm3-convex-create-component/markdown*