| name | cursor-plugin-convex-rule-use-components-for-encapsulation |
| description | Use Convex components to encapsulate features instead of mixing everything in one codebase. Components are self-contained, reusable, and maintainable. |
| metadata | {"version":"0.1.0"} |
Use Components for Encapsulation
When building features in Convex, prefer components over monolithic code. Components are self-contained mini-backends that encapsulate functionality.
What Are Components?
Components are:
- 🔒 Sandboxed - Can't access your main app's tables unless explicitly passed
- 📦 Self-contained - Include their own schema, functions, and data
- 🔄 Reusable - Can be used across multiple projects
- 🧩 Composable - Multiple components work as siblings
- 📚 npm-installable - Install from npm or use locally
Think of them as: Microservices within your Convex backend, but without the deployment complexity.
When to Use Components
✅ Use Components For:
Feature Encapsulation:
- Authentication/authorization
- File storage
- Rate limiting
- Analytics/tracking
- Notifications
- Search functionality
- Workflow orchestration
- AI agents
Reusable Patterns:
- Multi-tenant isolation
- Audit logging
- Caching layers
- Background job queues
- Event sourcing
Third-Party Integrations:
- Stripe payments
- SendGrid emails
- Cloudflare R2 storage
- External API wrappers
❌ Don't Use Components For:
- Core domain models that are tightly coupled
- One-off functionality specific to your app
- Simple utility functions (use convex-helpers instead)
Component vs Monolithic Code
❌ Without Components (Monolithic)
export const uploadFile = mutation({
handler: async (ctx, args) => {
},
});
✅ With Components (Encapsulated)
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,
ratelimit,
audit,
},
});
import { components } from "./_generated/api";
export const uploadFile = mutation({
handler: async (ctx, args) => {
await components.ratelimit.check(ctx, { key: ctx.user._id });
const fileId = await components.storage.store(ctx, args.file);
await components.audit.log(ctx, { action: "upload", fileId });
fileId;
},
});
Sibling Components Pattern
Multiple components at the same level (siblings) that work together:
export default defineApp({
components: {
auth: authComponent,
storage: storageComponent,
payments: paymentsComponent,
emails: emailComponent,
analytics: analyticsComponent,
},
});
export const createSubscription = mutation({
handler: async (ctx, args) => {
const user = await components.auth.getCurrentUser(ctx);
const subscription = await components.payments.createSubscription(ctx, {
userId: user._id,
plan: args.plan,
});
await components.analytics.track(ctx, {
event: "subscription_created",
userId: user._id,
});
await components.emails.send(ctx, {
to: user.email,
template: ,
});
subscription;
},
});
Benefits:
- Each component handles one concern
- Components can't interfere with each other
- Easy to replace one component without affecting others
- Clear boundaries between features
Installing Components
From npm (Official Components)
npm install @convex-dev/ratelimiter
npm install @convex-dev/storage
npm install @convex-dev/agent
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,
},
});
Local Components (Your Own)
mkdir -p convex/components/audit
import { defineComponent } from "convex/server";
export default defineComponent("audit");
export default defineSchema({
auditLogs: defineTable({
userId: v.id("users"),
action: v.string(),
timestamp: v.number(),
metadata: v.any(),
}).index("by_user", ["userId"]),
});
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(),
});
},
});
import { defineApp } from "convex/server";
import audit from "./components/audit/convex.config";
export default defineApp({
components: {
audit,
},
});
Official Components to Use
Browse the Component Directory for:
Authentication:
@convex-dev/better-auth - Better Auth integration
Storage:
@convex-dev/r2 - Cloudflare R2 file storage
Payments:
@convex-dev/polar - Polar billing/subscriptions
AI:
@convex-dev/agent - AI agent workflows
Backend 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 migrations
Creating Your Own Components
When to create a component:
-
Feature is self-contained
- Has its own data model
- Doesn't need direct access to main app tables
- Can work independently
-
You'll reuse it
- Across multiple projects
- In different contexts
- Share with team/community
-
Clear boundaries
- Well-defined API surface
- Minimal coupling to main app
- Can be versioned independently
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 code
Component Communication
✅ Good: Parent → Component
import { components } from "./_generated/api";
export const createUser = mutation({
handler: async (ctx, args) => {
const userId = await ctx.db.insert("users", args);
await components.analytics.track(ctx, {
event: "user_created",
userId,
});
},
});
✅ Good: Component → Parent (via passed data)
import { components } from "./_generated/api";
await components.notifications.send(ctx, {
userId: user._id,
message: "Welcome!",
});
❌ Bad: Component directly accessing parent tables
export const notify = mutation({
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
},
});
❌ Bad: Sibling → Sibling directly
Migration Strategy
From Monolithic to Components
Step 1: Identify feature boundaries
Current: Everything in convex/
Target: Features as components
Step 2: Extract one feature as component
mkdir -p convex/components/analytics
Step 3: Update main app to use component
import analytics from "./components/analytics/convex.config";
export default defineApp({
components: { analytics },
});
Step 4: Repeat for other features
Benefits:
- Incremental migration (no big bang rewrite)
- Test each component independently
- Can mix monolithic + components during transition
Best Practices
-
One concern per component
- auth component handles ONLY auth
- storage component handles ONLY storage
- Don't create "utils" component with everything
-
Clear API surface
- Export only what's needed
- Keep internals private
- Document component's public functions
-
Minimize coupling
- Pass data as arguments, don't access parent tables
- Make components work independently
- Avoid tight coupling between siblings
-
Version your components
- Use semantic versioning
- Document breaking changes
- Allow multiple versions if needed
-
Test components in isolation
- Each component can be tested separately
- Mock external dependencies
- Integration tests at app level
Examples from the Wild
Multi-tenant SaaS:
components: {
auth: authComponent,
organizations: orgComponent,
billing: billingComponent,
analytics: analyticsComponent,
emails: emailComponent,
}
E-commerce:
components: {
cart: cartComponent,
inventory: inventoryComponent,
orders: ordersComponent,
payments: paymentsComponent,
shipping: shippingComponent,
}
AI Application:
components: {
agent: agentComponent,
embeddings: embeddingsComponent,
documents: documentsComponent,
chat: chatComponent,
}
Quick Decision Tree
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 fine
Learn More
Checklist