Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Guide to using Convex components for feature encapsulation. Learn about sibling components, creating your own, and when to use components vs monolithic code.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 107% | 0% |
| case-23 | ✗→✓ | ▲ Improved | 162% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 216% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 754% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 156% | 0% |
Use components to encapsulate features and build maintainable, reusable backends.
Components are self-contained mini-backends that bundle:
Think of them as: npm packages for your backend, or microservices without the deployment complexity.
convex/
├── users.ts (500 lines)
├── files.ts (600 lines - upload, storage, permissions, rate limiting)
├── payments.ts (400 lines - Stripe, webhooks, billing)
├── notifications.ts (300 lines)
└── analytics.ts (200 lines)
Total: One big codebase, everything mixed togetherconvex/
├── components/
│ ├── storage/ (File uploads - reusable)
│ ├── billing/ (Payments - reusable)
│ ├── notifications/ (Alerts - reusable)
│ └── analytics/ (Tracking - reusable)
├── convex.config.ts (Wire components together)
└── domain/ (Your actual business logic)
├── users.ts (50 lines - uses components)
└── projects.ts (75 lines - uses components)
Total: Clean, focused, reusablebash# Official components from npm npm install @convex-dev/ratelimiter
typescriptimport { defineApp } from "convex/server"; import ratelimiter from "@convex-dev/ratelimiter/convex.config"; export default defineApp({ components: { ratelimiter, }, });
typescriptimport { components } from "./_generated/api"; export const createPost = mutation({ handler: async (ctx, args) => { // Use the component await components.ratelimiter.check(ctx, { key: `user:${ctx.user._id}`, limit: 10, period: 60000, // 10 requests per minute }); return await ctx.db.insert("posts", args); }, });
Multiple components working together at the same level:
typescript// convex.config.ts export default defineApp({ components: { // Sibling components - each handles one concern auth: authComponent, storage: storageComponent, payments: paymentsComponent, emails: emailComponent, analytics: analyticsComponent, }, });
typescript// convex/subscriptions.ts import { components } from "./_generated/api"; export const subscribe = mutation({ args: { plan: v.string() }, handler: async (ctx, args) => { // 1. Verify authentication (auth component) const user = await components.auth.getCurrentUser(ctx); // 2. Create payment (payments component) const subscription = await components.payments.createSubscription(ctx, { userId: user._id, plan: args.plan, amount: getPlanAmount(args.plan), }); // 3. Track conversion (analytics component) await components.analytics.track(ctx, { event: "subscription_created", userId: user._id, plan: args.plan, }); // 4. Send confirmation (emails component) await components.emails.send(ctx, { to: user.email, template: "subscription_welcome", data: { plan: args.plan }, }); // 5. Store subscription in main app await ctx.db.insert("subscriptions", { userId: user._id, paymentId: subscription.id, plan: args.plan, status: "active", }); return subscription; }, });
What this achieves:
Browse Component Directory:
Good reasons:
Not good reasons:
bashmkdir -p convex/components/notifications
typescript// convex/components/notifications/convex.config.ts import { defineComponent } from "convex/server"; export default defineComponent("notifications");
typescript// convex/components/notifications/schema.ts import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; export default defineSchema({ notifications: defineTable({ userId: v.id("users"), message: v.string(), read: v.boolean(), createdAt: v.number(), }) .index("by_user", ["userId"]) .index("by_user_and_read", ["userId", "read"]), });
typescript// convex/components/notifications/send.ts import { mutation } from "./_generated/server"; import { v } from "convex/values"; export const send = mutation({ args: { userId: v.id("users"), message: v.string(), }, handler: async (ctx, args) => { await ctx.db.insert("notifications", { userId: args.userId, message: args.message, read: false, createdAt: Date.now(), }); }, }); export const markRead = mutation({ args: { notificationId: v.id("notifications") }, handler: async (ctx, args) => { await ctx.db.patch(args.notificationId, { read: true }); }, });
typescript// convex/components/notifications/read.ts import { query } from "./_generated/server"; import { v } from "convex/values"; export const list = query({ args: { userId: v.id("users") }, handler: async (ctx, args) => { return await ctx.db .query("notifications") .withIndex("by_user", q => q.eq("userId", args.userId)) .order("desc") .collect(); }, }); export const unreadCount = query({ args: { userId: v.id("users") }, handler: async (ctx, args) => { const unread = await ctx.db .query("notifications") .withIndex("by_user_and_read", q => q.eq("userId", args.userId).eq("read", false) ) .collect(); return unread.length; }, });
typescript// convex.config.ts import { defineApp } from "convex/server"; import notifications from "./components/notifications/convex.config"; export default defineApp({ components: { notifications, // Your local component }, });
typescript// convex/tasks.ts - main app code import { components } from "./_generated/api"; export const completeTask = mutation({ args: { taskId: v.id("tasks") }, handler: async (ctx, args) => { const task = await ctx.db.get(args.taskId); await ctx.db.patch(args.taskId, { completed: true }); // Use your component await components.notifications.send(ctx, { userId: task.userId, message: `Task "${task.title}" completed!`, }); }, });
typescript// Main app calls component await components.storage.upload(ctx, file); await components.analytics.track(ctx, event);
typescript// Main app orchestrates multiple components await components.auth.verify(ctx); const file = await components.storage.upload(ctx, data); await components.notifications.send(ctx, message);
typescript// Pass IDs from parent's tables to component await components.audit.log(ctx, { userId: user._id, // From parent's users table action: "delete", resourceId: task._id, // From parent's tasks table }); // Component stores these as strings/IDs // but doesn't access parent tables directly
typescript// Inside component code - DON'T DO THIS const user = await ctx.db.get(userId); // Error! Can't access parent tables
Components can't call each other directly. If you need this, they should be in the main app or refactor the design.
typescript// convex.config.ts export default defineApp({ components: { auth: "@convex-dev/better-auth", organizations: "./components/organizations", billing: "./components/billing", storage: "@convex-dev/r2", analytics: "./components/analytics", emails: "./components/emails", }, });
Each component:
auth - User authentication & sessionsorganizations - Tenant isolation & permissionsbilling - Stripe integration & subscriptionsstorage - File uploads to R2analytics - Event tracking & metricsemails - Email sending via SendGridtypescriptexport default defineApp({ components: { cart: "./components/cart", inventory: "./components/inventory", orders: "./components/orders", payments: "@convex-dev/polar", shipping: "./components/shipping", recommendations: "./components/recommendations", }, });
typescriptexport default defineApp({ components: { agent: "@convex-dev/agent", embeddings: "./components/embeddings", documents: "./components/documents", chat: "./components/chat", workflow: "@convex-dev/workflow", }, });
Step 1: Identify Features
Current monolith:
- File uploads (mixed with main app)
- Rate limiting (scattered everywhere)
- Analytics (embedded in functions)Step 2: Extract One Feature
bash# Create component mkdir -p convex/components/storage # Move storage code to component # Update imports in main app
Step 3: Test Independently
bash# Component has its own tests # No coupling to main app
Step 4: Repeat Extract other features incrementally.
Each component does ONE thing well:
typescript// Export only what's needed export { upload, download, delete } from "./storage"; // Keep internals private // (Don't export helper functions)
typescript// ✅ Good: Pass data as arguments await components.audit.log(ctx, { userId: user._id, action: "delete" }); // ❌ Bad: Component accesses parent tables // (Not even possible, but shows the principle)
json{ "name": "@yourteam/notifications-component", "version": "1.0.0" }
Include README with:
bash# Make sure component is in convex.config.ts # Run: npx convex dev
This is by design! Components are sandboxed.
Pass data as arguments instead.Each component has isolated tables.
Components can't see each other's data.npm install @convex-dev/component-nameconvex.config.tsRemember: Components are about encapsulation and reusability. When in doubt, prefer components over monolithic code!
Other measured skills in the registry, with their headline benchmark lift.