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.
.claude/skills/kunanonj-cursor-plugin-convex-rule-use-components-for-encapsulation/SKILL.md| 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.ts| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 16,920 | 8,154 | -52% | 1 | 1 | 0% | 3,935 | 4,865 | +24% | 0 | 0 | — |
case-02 | fail→pass | 17,081 | 11,875 | -30% | 1 | 1 | 0% | 3,330 | 5,690 | +71% | 0 | 0 | — |
case-07 | fail→fail | 11,963 | 6,945 | -42% | 1 | 1 | 0% | 2,287 | 4,421 | +93% | 0 | 0 | — |
case-03 | pass→pass | 17,264 | 11,423 | -34% | 1 | 1 | 0% | 3,803 | 5,692 | +50% | 0 | 0 | — |
case-04 | fail→pass | 15,825 | 8,381 | -47% | 1 | 1 | 0% | 3,007 | 4,874 | +62% | 0 | 0 | — |
case-05 | fail→pass | 15,622 | 7,062 | -55% | 1 | 1 | 0% | 3,429 | 4,423 | +29% | 0 | 0 | — |
case-06 | fail→pass | 7,441 | 5,752 | -23% | 1 | 1 | 0% | 1,419 | 4,265 | +201% | 0 | 0 | — |
case-08 | fail→pass | 13,892 | 7,678 | -45% | 1 | 1 | 0% | 2,498 | 4,697 | +88% | 0 | 0 | — |
case-09 | pass→pass | 13,051 | 7,377 | -43% | 1 | 1 | 0% | 2,516 | 4,418 | +76% | 0 | 0 | — |
case-10 | pass→pass | 11,431 | 4,972 | -57% | 1 | 1 | 0% | 2,475 | 4,062 | +64% | 0 | 0 | — |
case-11 | fail→pass | 15,864 | 6,741 | -58% | 1 | 1 | 0% | 3,281 | 4,480 | +37% | 0 | 0 | — |
case-12 | fail→pass | 18,161 | 12,838 | -29% | 1 | 1 | 0% | 3,743 | 5,842 | +56% | 0 | 0 | — |
case-13 | pass→pass | 10,680 | 7,021 | -34% | 1 | 1 | 0% | 2,200 | 4,691 | +113% | 0 | 0 | — |
case-14 | pass→pass | 12,867 | 8,566 | -33% | 1 | 1 | 0% | 2,615 | 4,774 | +83% | 0 | 0 | — |
case-15 | pass→pass | 17,387 | 15,186 | -13% | 1 | 1 | 0% | 2,725 | 5,446 | +100% | 0 | 0 | — |
case-16 | pass→pass | 10,221 | 5,385 | -47% | 1 | 1 | 0% | 2,119 | 4,067 | +92% | 0 | 0 | — |
case-17 | fail→pass | 13,767 | 7,607 | -45% | 1 | 1 | 0% | 2,744 | 4,518 | +65% | 0 | 0 | — |
case-18 | fail→pass | 18,186 | 10,967 | -40% | 1 | 1 | 0% | 3,509 | 5,255 | +50% | 0 | 0 | — |
case-19 | pass→pass | 17,596 | 14,193 | -19% | 1 | 1 | 0% | 3,306 | 6,402 | +94% | 0 | 0 | — |
case-20 | pass→pass | 11,954 | 5,215 | -56% | 1 | 1 | 0% | 2,088 | 3,997 | +91% | 0 | 0 | — |
case-21 | pass→pass | 13,942 | 6,266 | -55% | 1 | 1 | 0% | 2,220 | 4,288 | +93% | 0 | 0 | — |
case-22 | pass→pass | 12,115 | 6,995 | -42% | 1 | 1 | 0% | 1,981 | 4,443 | +124% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +45 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.