Full-Stack Business Application

Silk House Billing Portal

A double-entry accounting system for a 5-firm silk saree business. Automates GST compliance, bank reconciliation, and multi-firm consolidated reporting.

📅 2026 ⚡ TypeScript · React · Supabase 📈 Status: Live

01 The Problem

A real-world silk saree business running 5 independent firms (DSS, SMSS, LKC, SDS, NSS) — each with its own bank account, GSTIN, and state registration. Sales are B2C; payment arrives 1–2 months late. Transactions span both inter-state (IGST) and intra-state (CGST+SGST) tax regimes. The accountant was managing everything with physical ledgers.

Every month-end meant hours of manual reconciliation, hand-written GST return calculations, and no real-time visibility into debtor positions, firm-level profitability, or tax liability. Errors were common, and cross-firm consolidation was a spreadsheet nightmare.

Context

This wasn't a hypothetical project — it replaced a real, paper-based accounting workflow mid-financial-year for a business handling crores in annual turnover.

02 Architecture

The system follows a modern full-stack architecture: a React single-page application backed by Supabase (PostgreSQL with Row-Level Security), with an optional Tauri desktop wrapper for offline access.

┌──────────────────────────────────────────────────────┐
│                Silk House Portal                      │
│  ┌────────────────────────────────────────────────┐  │
│  │           React SPA (Vite + TypeScript)        │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐       │  │
│  │  │ Dashboard │ │Invoicing │ │ Reports  │       │  │
│  │  └──────────┘ └──────────┘ └──────────┘       │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐       │  │
│  │  │ Purchases│ │   Auth   │ │ Settings │       │  │
│  │  └──────────┘ └──────────┘ └──────────┘       │  │
│  └──────────────────┬─────────────────────────────┘  │
│                     │ HTTP + JWT                      │
│  ┌──────────────────▼─────────────────────────────┐  │
│  │          Supabase (PostgreSQL + RLS)            │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐       │  │
│  │  │  Sales   │ │ Returns  │ │  Bank    │       │  │
│  │  │ Invoices │ │Credit No│ │Transacts │       │  │
│  │  ├──────────┤ ├──────────┤ ├──────────┤       │  │
│  │  │Purchases │ │  Firms   │ │   GST    │       │  │
│  │  │          │ │   (5)    │ │ Returns  │       │  │
│  │  └──────────┘ └──────────┘ └──────────┘       │  │
│  └────────────────────────────────────────────────┘  │
│                                                      │
│  ┌────────────────────────────────────────────────┐  │
│  │       Tauri Desktop Wrapper (optional)         │  │
│  └────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────┘
Design Decision

Supabase Row-Level Security was chosen to enforce firm-level data isolation at the database layer — ensuring that even if the front-end had a bug, a user from DSS could never see LKC's transactions.

03 Tech Stack

Every technology was chosen with a specific purpose: type safety across the stack, fast iteration, accessible UI, and proven production databases.

Layer Technology Purpose
Language TypeScript Full-stack type safety — share types between front-end and database
Front-end React 19 + Vite Fast dev server, modern React patterns, tree-shakeable builds
Back-end / Database Supabase (PostgreSQL) Managed Postgres, built-in auth, auto-generated REST API
ORM Prisma Declarative schema, migrations, type-safe queries
UI Components Radix UI Accessible, unstyled primitives for modals, selects, dropdowns
Styling Tailwind CSS Utility-first, consistent design tokens, rapid prototyping
Testing Vitest 50+ tests — components, hooks, lib utilities, page integration
Desktop Tauri Optional native wrapper for offline access / local backups

04 Key Challenges

  1. 1. Double-entry for delayed payments Every sale books immediate credit to revenue and debit to the debtor's account. Payment reconciliation — which can arrive 60 days late — posts against the original invoice. The ledger must show accurate debtor positions at all times, even mid-cycle.
    Solution

    Each invoice carries a unique payment status derived from linked bank transactions. A materialised view computes real-time ageing summaries for the dashboard without hammering the OLTP tables.

  2. 2. Multi-firm consolidation Five separate GSTINs, bank accounts, and state registrations. Each firm needs its own trial balance while a consolidated view sums across all five. Cross-firm inventory or fund transfers need careful double-sided journal entries.
    Solution

    Every row across all transaction tables is scoped by firm_id. A central firms table drives per-firm dashboards via RLS policies. Consolidated reports are computed by summing across firms with the same SQL views — no separate consolidation logic needed.

  3. 3. GST compliance Every sale must be classified as inter-state (IGST) or intra-state (CGST+SGST) based on the shipping address vs. the firm's registered state. Accurate return filing (GSTR-1, GSTR-3B) depends on perfect classification — a single wrong tax code cascades into penalties.
    Solution

    Tax regime is computed automatically from the selected customer's state and the firm's registered state. The UI displays the applicable rates before the invoice is saved, preventing errors at the point of entry.

  4. 4. Real business constraints The system replaced physical ledgers mid-financial-year. Migration had to be lossless — every open invoice, every uncleared cheque, every previous month's return. And the UI had to be intuitive enough for a non-technical accountant to use daily.
    Solution

    A structured import process with validation reports, plus close collaboration with the accountant during UAT. The interface uses familiar metaphors (ledger, journal, voucher) and keyboard shortcuts for rapid data entry.

By the Numbers

5 Firms (DSS, SMSS, LKC, SDS, NSS)
Auto CGST+SGST / IGST on every tx
Live Real ledgers, actual accountant
50+ Vitest tests across the stack

Code Highlight

The GST engine determines tax type from shipping addresses, then computes CGST+SGST or IGST with deterministic rounding.

export function calculateGST(input: GSTInput): GSTCalculation {
  const { firmStateCode, partyStateCode,
          taxableAmount, hsnCode, overrideRate } = input;
  // Intra-state → CGST+SGST, Inter-state → IGST
  const isIntraState = firmStateCode === partyStateCode;
  const gstType = isIntraState ? 'CGST_SGST' : 'IGST';
  const fullRate = overrideRate ?? getGstRate(hsnCode);
  const round2 = (n: number) => Math.round(n * 100) / 100;

  if (isIntraState) {
    const halfRate = fullRate / 2;
    const cgstAmt = round2(taxableAmount * halfRate / 100);
    const sgstAmt = round2(taxableAmount * halfRate / 100);
    return {
      gst_type: 'CGST_SGST',
      cgst_rate: halfRate, sgst_rate: halfRate, igst_rate: 0,
      cgst_amount: cgstAmt, sgst_amount: sgstAmt, igst_amount: 0,
      net_amount: taxableAmount + cgstAmt + sgstAmt,
    };
  }
  // IGST for inter-state — single rate applied
  ...
}

05 Results & Impact

The portal went from zero to live production in active use by the business accountant. Here's what changed:

📝 Physical ledgers eliminated for all 5 firms
📈 Real-time dashboards: GST liability, sales, bank reconciliation
📦 Purchase tracking module added for full-cycle accounting
Tauri desktop build for offline access
🔒 Supabase RLS ensures firm-level data isolation
GST return data extracted on demand — no manual calc

06 What I Learned

Building a real-world accounting system for a real accountant taught me things no tutorial ever could:

Takeaway

The most satisfying line of code I wrote wasn't a clever React hook or a fancy SQL window function. It was the migration script that successfully imported 6 months of physical ledger data — and the accountant's message: "The numbers match."