launchthat
From Monolith to 34 Plugins: How We Built an Extensible Platform Without Losing Our Minds
A customer asked for Monday.com integration. We could have hard-coded it into the core. Instead, we built a plugin system — and 18 months later we have 34 plugins serving 27 apps.
We did not start with 34 plugins. We started with one monolithic app that tried to do everything, and a customer request that forced us to rethink the entire architecture.
The customer wanted Monday.com integration for project management. Our platform had its own project management built in. The options were:
Option 1: Build Monday.com into the core. We would maintain two project management systems forever. Code paths would fork. Bug fixes would need to happen twice.
Option 2: Offer it as an add-on. We would still maintain both systems. The integration would be an afterthought that nobody owned.
Option 3: Build a plugin system. Monday.com becomes one implementation of a "project management" interface. The core defines the interface. Plugins implement it. Customers choose which implementation they want.
We chose Option 3. It took three months to build the plugin system. Eighteen months later, we have 34 plugins and a platform that customers actually want to extend rather than work around.

The core principle: interface over implementation
Every plugin solves a problem. The core defines the problem space without dictating the solution:
export interface ProjectManagementProvider {
createProject(name: string): Promise<Project>;
createTask(projectId: string, name: string): Promise<Task>;
moveTask(taskId: string, column: string): Promise<void>;
getTasks(projectId: string): Promise<Task[]>;
}
The Monday.com plugin implements this interface by wrapping their API. A Notion plugin could implement the same interface differently. The core does not know or care which one is active — it just knows about projects and tasks.
This separation was the single most important architectural decision we made. Everything else follows from it.
The three parts of every plugin
We iterated on the plugin interface four times before landing on a structure that worked. Every plugin has exactly three parts:
1. Convex component (backend)
Each plugin gets its own scoped database tables, API routes, real-time subscriptions, and background jobs:
export const stripe = defineComponent({
name: "stripe",
routers: {
checkout: checkoutRouter,
webhooks: webhookRouter,
payouts: payoutRouter,
},
});
The scoping is critical. A plugin's tables are namespaced to its component. Uninstalling a plugin hides its features without destroying data. Reinstalling it brings everything back.
2. Frontend module (React components)
Apps import what they need and compose it into their UI:
export { CheckoutButton } from "./CheckoutButton";
export { SubscriptionStatus } from "./SubscriptionStatus";
export { InvoiceHistory } from "./InvoiceHistory";
export { PricingTable } from "./PricingTable";
A server-side only app does not need to bundle React components. The exports structure makes tree-shaking work correctly.
3. Package exports
{
"exports": {
".": "./dist/index.js",
"./convex/component": "./dist/convex/component/index.js",
"./frontend": "./dist/frontend/index.js",
"./convex.config": "./dist/convex/component/convex.config.js"
}
}
Real integration patterns
Stripe: webhooks + background jobs
Stripe follows a specific flow: user clicks Subscribe → server creates Checkout Session → Stripe processes payment → webhook fires → database updates → follow-up actions trigger.
export const handleWebhook = httpAction(async (ctx, request) => {
const sig = request.headers.get("stripe-signature")!;
const body = await request.text();
const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
switch (event.type) {
case "checkout.session.completed":
await ctx.runMutation(stripeRouter.subscriptions.activate, {
sessionId: event.data.object.id,
});
break;
}
});
The webhook handler verifies the Stripe signature before processing anything. Every event is handled idempotently so replay is safe.
Discord: OAuth + bot runtime
Discord has two paths — OAuth for user authentication and a persistent bot connection for server interactions. We run Discord bots as a separate runtime that maintains WebSocket connections to Discord's gateway and relays events to our Convex backend.
Email: template system + delivery tracking
Users edit email templates in the admin UI with {{variable}} interpolation. The system validates variables and previews output before sending. Each send is tracked through delivery webhooks from the provider.
The plugin registry
Apps discover plugins through a registry that powers the marketplace UI, dependency resolution, permissions, and update notifications:
export const pluginRegistry = {
crm: {
name: "CRM",
description: "Customer relationship management",
version: "1.0.0",
component: () => import("launchthat-plugin-crm"),
dependencies: [],
permissions: ["contacts", "companies", "deals"],
},
stripe: {
name: "Stripe",
description: "Payment processing",
version: "1.2.0",
component: () => import("launchthat-plugin-ecommerce-stripe"),
dependencies: ["ecommerce"],
permissions: ["billing", "subscriptions"],
},
};
Dependency resolution means enabling CRM automatically enables Contacts. Permission scoping means a plugin cannot access data it has not declared in its manifest.
What we would do differently
Define the plugin interface earlier. We iterated on it four times. Each iteration meant breaking changes for all existing plugins. The third rewrite was particularly painful because we had 12 plugins by then.
Standardize on one schema language. Some plugins defined schemas in raw TypeScript, others in Zod. We should have picked one on day one.
Build the registry before plugins. We manually tracked plugins in Notion for the first six months. The registry should have existed from the start.
Document the mental model, not just the API. Plugins are a different way of thinking about software. New contributors needed "why would I build a plugin?" guides more than they needed API reference docs.
The payoff
The plugin architecture turned our monolithic platform into an ecosystem. Customers get exactly the features they need. We maintain code once. New plugins ship without touching the core.
The Monday.com customer who started all of this? They are still using the platform. They have since added the Stripe plugin and the CRM plugin to their workspace. Each one took minutes to enable, and none of them required us to write custom code.
That is the power of extensibility done right.
Want to see how this was built?
See the portal projectWant to see how this was built?
Browse all posts