K
Zansify
Modules

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.json at 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.