Module Overview
How Kasify's module system works and how to extend it.
What is a module?
Every feature in Kasify is a module — a self-contained unit that owns its database schema, API router, and UI. Modules are isolated, predictable, and composable.
This is directly inspired by Magento's module architecture, adapted for a modern TypeScript + tRPC stack.
Module names follow the Vendor_ModuleName convention — always PascalCase, always prefixed:
Kasify_Catalog
Kasify_Orders
Kasify_AIAssistant
Before adding any new feature, always check
modules.jsonat the project root to see if a module for that domain already exists. Extend it rather than creating a new one.
Built-in modules
| Module | Owns | Description |
|---|---|---|
| Kasify_Catalog | Products, variants, images, collections | Full inventory management |
| Kasify_Orders | Orders, line items, status | Order lifecycle and fulfillment |
| Kasify_Customers | Customer accounts, addresses | Auth, profiles, order history |
| Kasify_Coupons | Discount codes, usage tracking | Promotional pricing |
| Kasify_Shipping | Shipping rates and methods | Carrier integrations |
| Kasify_Reviews | Product reviews, moderation | Social proof |
| Kasify_Storefront | Themes, pages, page builder | Visual storefront customisation |
| Kasify_Email | Templates, SMTP, logs | Transactional email |
| Kasify_AIAssistant | Chatbot, descriptions, recommendations | Multi-provider AI |
| Kasify_Integrations | Third-party connectors | Stripe, PayFast, couriers |
| Kasify_Analytics | Revenue, orders, conversion | Store reporting |
| Kasify_Collections | Collections, categories | Product organisation |
What a module owns
Each module follows a consistent file ownership pattern:
apps/api/src/routers/<moduleName>.ts ← API router (required)
apps/dashboard/src/app/[slug]/<module>/ ← Dashboard page (if UI)
apps/storefront/src/components/<module>/ ← Storefront component (if customer-facing)
packages/db/src/schema/<module>.ts ← DB schema (if persistent)
Rule: Never split a module's core logic across multiple routers. If a feature grows, add procedures to the existing router.
Adding a new module
1. Check modules.json
cat modules.json | grep "MyFeature"
If it exists — extend the existing module. If not, continue.
2. Add the DB schema
// packages/db/src/schema/myFeature.ts
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core";
import { tenants } from "./tenants";
export const myFeatureItems = pgTable("my_feature_items", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: uuid("tenant_id").notNull().references(() => tenants.id, { onDelete: "cascade" }),
name: text("name").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Then export it from the schema index:
// packages/db/src/schema/index.ts
export * from "./myFeature";
Push the schema to your database:
pnpm db:push
3. Create the API router
// apps/api/src/routers/myFeature.ts
// Module: Kasify_MyFeature
import { z } from "zod";
import { router, protectedProcedure } from "../trpc";
import { myFeatureItems } from "@kasify/db";
import { eq } from "drizzle-orm";
export const myFeatureRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
return ctx.db
.select()
.from(myFeatureItems)
.where(eq(myFeatureItems.tenantId, ctx.tenantId));
}),
create: protectedProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(async ({ ctx, input }) => {
const [item] = await ctx.db
.insert(myFeatureItems)
.values({ tenantId: ctx.tenantId, name: input.name })
.returning();
return item;
}),
});
4. Register the router
// apps/api/src/router.ts
import { myFeatureRouter } from "./routers/myFeature";
export const appRouter = router({
// ...existing routers
myFeature: myFeatureRouter,
});
5. Register in modules.json
{
"name": "Kasify_MyFeature",
"description": "A brief description of what this module does.",
"status": "active",
"files": [
"apps/api/src/routers/myFeature.ts",
"packages/db/src/schema/myFeature.ts"
]
}
Module interactions
When two modules need to share data, import directly from the other module's schema. Do not create bridge routers.
// Kasify_Orders reading Kasify_Catalog inventory
import { products } from "@kasify/db"; // from Kasify_Catalog's schema
// Inside the orders router:
const product = await ctx.db
.select()
.from(products)
.where(eq(products.id, input.productId));
This keeps coupling explicit and traceable — grep for the import to find every cross-module dependency.