Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use Convex components to encapsulate features instead of mixing everything in one codebase. Components are self-contained, reusable, and maintainable.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 24% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 29% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 201% | 0% |
When building features in Convex, prefer components over monolithic code. Components are self-contained mini-backends that encapsulate functionality.
Components are:
Think of them as: Microservices within your Convex backend, but without the deployment complexity.
Feature Encapsulation:
Reusable Patterns:
Third-Party Integrations:
typescript// Everything mixed in convex/files.ts export const uploadFile = mutation({ handler: async (ctx, args) => { // File upload logic // Rate limiting logic // Audit logging logic // Storage logic // All in one file! }, }); // Hard to: // - Reuse in other projects // - Test in isolation // - Update without breaking other features // - Share with team
typescript// convex.config.ts import { defineApp } from "convex/server"; import storage from "@convex-dev/storage"; import ratelimit from "@convex-dev/ratelimiter"; import audit from "./audit/convex.config"; export default defineApp({ components: { storage, // Sibling component #1 ratelimit, // Sibling component #2 audit, // Sibling component #3 }, }); // convex/files.ts - clean and focused import { components } from "./_generated/api"; export const uploadFile = mutation({ handler: async (ctx, args) => { // Check rate limit (component) await components.ratelimit.check(ctx, { key: ctx.user._id }); // Store file (component) const fileId = await components.storage.store(ctx, args.file); // Log action (component) await components.audit.log(ctx, { action: "upload", fileId }); return fileId; }, }); // Each component: // - Maintained separately // - Reusable across projects // - Testable in isolation // - Can be updated independently
Multiple components at the same level (siblings) that work together:
typescript// convex.config.ts export default defineApp({ components: { // These are sibling components auth: authComponent, storage: storageComponent, payments: paymentsComponent, emails: emailComponent, analytics: analyticsComponent, }, }); // Usage - siblings don't see each other's internals export const createSubscription = mutation({ handler: async (ctx, args) => { // 1. Verify user (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, }); // 3. Track event (analytics component) await components.analytics.track(ctx, { event: "subscription_created", userId: user._id, }); // 4. Send confirmation (emails component) await components.emails.send(ctx, { to: user.email, template: "subscription_confirmation", }); return subscription; }, });
Benefits:
bashnpm install @convex-dev/ratelimiter npm install @convex-dev/storage npm install @convex-dev/agent
typescript// convex.config.ts import { defineApp } from "convex/server"; import ratelimiter from "@convex-dev/ratelimiter/convex.config"; import storage from "@convex-dev/storage/convex.config"; export default defineApp({ components: { ratelimiter, storage, }, });
bash# Create a component directory mkdir -p convex/components/audit
typescript// convex/components/audit/convex.config.ts import { defineComponent } from "convex/server"; export default defineComponent("audit"); // convex/components/audit/schema.ts export default defineSchema({ auditLogs: defineTable({ userId: v.id("users"), action: v.string(), timestamp: v.number(), metadata: v.any(), }).index("by_user", ["userId"]), }); // convex/components/audit/logs.ts export const log = mutation({ args: { userId: v.id("users"), action: v.string(), metadata: v.any(), }, handler: async (ctx, args) => { await ctx.db.insert("auditLogs", { ...args, timestamp: Date.now(), }); }, });
typescript// convex.config.ts - use your local component import { defineApp } from "convex/server"; import audit from "./components/audit/convex.config"; export default defineApp({ components: { audit, // Local component as sibling }, });
Browse the Component Directory for:
Authentication:
@convex-dev/better-auth - Better Auth integrationStorage:
@convex-dev/r2 - Cloudflare R2 file storagePayments:
@convex-dev/polar - Polar billing/subscriptionsAI:
@convex-dev/agent - AI agent workflowsBackend Utilities:
@convex-dev/ratelimiter - Rate limiting@convex-dev/aggregate - Aggregations@convex-dev/action-cache - Action caching@convex-dev/sharded-counter - Distributed counters@convex-dev/migrations - Data migrationsWhen to create a component:
Structure:
convex/
├── components/
│ ├── notifications/
│ │ ├── convex.config.ts
│ │ ├── schema.ts
│ │ ├── send.ts
│ │ └── read.ts
│ ├── analytics/
│ │ ├── convex.config.ts
│ │ ├── schema.ts
│ │ └── track.ts
│ └── search/
│ ├── convex.config.ts
│ ├── schema.ts
│ └── index.ts
├── convex.config.ts # App configuration
└── ... # Main app codetypescript// Main app calls component import { components } from "./_generated/api"; export const createUser = mutation({ handler: async (ctx, args) => { const userId = await ctx.db.insert("users", args); // Parent app can call component await components.analytics.track(ctx, { event: "user_created", userId, }); }, });
typescript// Component receives parent data as arguments import { components } from "./_generated/api"; // Pass user ID to component await components.notifications.send(ctx, { userId: user._id, // From parent's user table message: "Welcome!", });
typescript// Inside component - DON'T DO THIS export const notify = mutation({ handler: async (ctx, args) => { // ❌ Can't access parent's users table const user = await ctx.db.get(args.userId); // Error! }, });
typescript// ❌ Components can't call each other directly // Must go through parent app
Step 1: Identify feature boundaries
Current: Everything in convex/
Target: Features as componentsStep 2: Extract one feature as component
bashmkdir -p convex/components/analytics # Move analytics code to component
Step 3: Update main app to use component
typescriptimport analytics from "./components/analytics/convex.config"; export default defineApp({ components: { analytics }, });
Step 4: Repeat for other features
Benefits:
Multi-tenant SaaS:
typescriptcomponents: { auth: authComponent, // User authentication organizations: orgComponent, // Multi-tenant isolation billing: billingComponent, // Stripe integration analytics: analyticsComponent, // Event tracking emails: emailComponent, // SendGrid wrapper }
E-commerce:
typescriptcomponents: { cart: cartComponent, // Shopping cart inventory: inventoryComponent, // Stock management orders: ordersComponent, // Order processing payments: paymentsComponent, // Payment processing shipping: shippingComponent, // Shipping integration }
AI Application:
typescriptcomponents: { agent: agentComponent, // AI agent workflows embeddings: embeddingsComponent, // Vector storage documents: documentsComponent, // Document processing chat: chatComponent, // Chat interface }
Need to add a feature?
├─ Is it self-contained? ─→ YES ─→ Use component
│ └─ NO ─→ Add to main app
│
├─ Will you reuse it? ─→ YES ─→ Use component
│ └─ NO ─→ Consider main app
│
├─ Third-party integration? ─→ YES ─→ Use component
│ └─ NO ─→ Continue checking
│
└─ Complex feature with own data model? ─→ YES ─→ Use component
└─ NO ─→ Main app is fineconvex.config.tsOther measured skills in the registry, with their headline benchmark lift.