MS

0
Skip to main content

How I Built a Full SaaS for Brick Manufacturing Factories

March 28, 2025 (1y ago)

Most SaaS case studies are about building the next project management tool or CRM. This one is about brick factories. Real ones — with raw material inflow, production tracking, per-customer pricing, and GST-compliant invoicing.

I built Brick Management System as a full SaaS product that now manages multi-factory operations. This post covers the technical decisions, architecture, and lessons from building a domain-specific SaaS from scratch.

The Problem

Brick manufacturing in India is a largely undigitized industry. Factory owners track operations on paper or in basic spreadsheets. The challenges:

No existing tool fits this niche. Generic inventory tools don't handle per-customer pricing. Generic invoicing tools don't understand raw material to production workflows.

Architecture Decisions

Tech Stack

Frontend: React + TypeScript + Tailwind CSS + Redux Toolkit + RTK Query
Backend: Node.js + Express + Prisma + PostgreSQL
PDF Generation: PDFKit
Build: Turborepo monorepo

Why this stack:

This is the same TypeScript full stack approach I follow across all my projects — shared types, Zod validation at boundaries, and Prisma for the database layer.

Database Design

The data model centers around these entities:

Factory → has many → Materials (inflow)
Factory → has many → Productions (daily output)
Factory → has many → Customers
Customer → has many → PricingRules (per brick type)
Customer → has many → Sales
Sale → has many → LineItems
Sale → generates → Invoice (with GST)
Factory → belongs to → Organization
Organization → has many → Users (with roles)

Per-customer pricing was the trickiest part. Each customer negotiates different rates for different brick types. The pricing table stores these as:

model CustomerPricing {
  id          String   @id @default(cuid())
  customerId  String
  brickType   String
  pricePerUnit Decimal @db.Decimal(10, 2)
  effectiveFrom DateTime
  customer    Customer @relation(fields: [customerId], references: [id])
}

When creating a sale, the system automatically looks up the customer's negotiated price for each brick type. If no custom price exists, it falls back to the factory's default price.

Role-Based Access Control

Three roles with distinct permissions:

Role Access Level
Owner Full access to all factories, all data, user management
Admin Full access to assigned factory only
Consultant Read-only access + commission tracking across assigned factories

The consultant role was interesting — factory owners hire consultants who refer customers and earn commissions on sales. The system tracks referrals and calculates commissions automatically.

RBAC is implemented as middleware:

function requireRole(...roles: Role[]) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = req.user;
    if (!roles.includes(user.role)) {
      return res.status(403).json({ error: "Insufficient permissions" });
    }
    // For admin role, verify factory assignment
    if (user.role === "admin" && req.params.factoryId) {
      const hasAccess = await checkFactoryAccess(user.id, req.params.factoryId);
      if (!hasAccess) {
        return res.status(403).json({ error: "No access to this factory" });
      }
    }
    next();
  };
}

GST-Compliant Invoice Generation

Indian businesses need GST-compliant invoices with specific fields: GSTIN, HSN codes, CGST/SGST/IGST breakdowns, and a specific format. I generate these server-side with PDFKit:

async function generateInvoice(saleId: string): Promise<Buffer> {
  const sale = await prisma.sale.findUnique({
    where: { id: saleId },
    include: {
      lineItems: true,
      customer: true,
      factory: { include: { organization: true } },
    },
  });
 
  const doc = new PDFDocument({ size: "A4", margin: 50 });
  // ... layout code for GST-compliant format
  // Includes: seller GSTIN, buyer GSTIN, HSN codes,
  // taxable value, CGST, SGST, IGST, total
  return doc;
}

The invoices include sequential numbering per financial year, QR codes for verification, and proper tax breakdowns based on whether the sale is intra-state (CGST + SGST) or inter-state (IGST).

Stock Tracking with Adjustment Workflows

Real factories have discrepancies between expected and actual stock. The system handles this with an adjustment workflow:

  1. Expected stock = raw material inflow - material used in production
  2. Actual stock is entered during physical counts
  3. Adjustments record the difference with a reason (wastage, theft, measurement error)
  4. Audit log tracks every adjustment with who made it and why

This was important because factory owners often discover stock doesn't match records. Rather than just letting them edit numbers, the adjustment workflow creates an audit trail.

Server-Side Pagination and Performance

With factories generating hundreds of sales per month, performance mattered. Key optimizations:

// Cursor-based pagination
const sales = await prisma.sale.findMany({
  take: pageSize + 1,
  cursor: cursor ? { id: cursor } : undefined,
  where: { factoryId },
  orderBy: { createdAt: "desc" },
  include: { customer: true, lineItems: true },
});
 
const hasMore = sales.length > pageSize;
const items = hasMore ? sales.slice(0, -1) : sales;

Multi-Factory Dashboard

Owners with multiple factories need a consolidated view. The dashboard shows:

Each widget makes a separate API call, and RTK Query handles the caching and loading states. The dashboard loads fast because data is fetched in parallel.

Lessons from Building Domain-Specific SaaS

1. Domain expertise is the moat. Any developer can build CRUD. Understanding that brick factories need per-customer pricing, GST compliance, and consultant commission tracking — that's what makes the product useful.

2. Talk to users before building. I spent time with factory owners understanding their workflows before writing any code. Features I thought were important (real-time dashboards) were less valued than features I almost skipped (stock adjustment audit trails).

3. Monorepo pays off for full stack SaaS. Sharing types and validation between frontend and backend eliminates an entire class of bugs. When I change a Prisma model, TypeScript errors immediately surface everywhere the type is used.

4. Start with the hardest feature. I built the invoicing system first because it had the most regulatory constraints. Everything else was easier after that.

5. PDFKit is underrated. Server-side PDF generation with full control over layout is powerful. No need for headless browsers or third-party PDF services.

What's Next

The system is production-ready and being used by factory owners. Next steps include:

Building niche SaaS is rewarding — the users genuinely need the tool, and there's no competition from generic solutions. If you're a full stack developer looking for a project, find an underserved industry and build for them.

You can see this project and others on my portfolio. For more technical deep dives, check out my blog.


I'm Manjodh Singh Saran — a full stack developer from Ludhiana, India. I build production SaaS products and mobile apps. Check out the Brick Management System live.

Related Articles

MS
Manjodh Singh Saran

Full Stack Developer · Ludhiana, India

Read more articles · View portfolio