A multi-tenant, full-featured invoicing and accounting SaaS designed for Indian businesses with GST compliance, multi-company support, role-based access, Razorpay payments, and a rich set of business reports.
SmartBooks is a cloud-native invoicing, accounting, and inventory management platform built for Indian small-to-medium businesses. It handles the full spectrum of day-to-day accounting:
| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite 5 |
| Styling | Tailwind CSS 3, shadcn/ui (Radix UI primitives) |
| State Management | React Context (AppContext, AuthContext, CompanyContext, LicenseContext), TanStack React Query |
| Routing | React Router DOM v6 |
| Forms | React Hook Form + Zod validation |
| Animations | Framer Motion |
| Charts | Recharts |
| Backend | Supabase (PostgreSQL, Auth, Edge Functions, Storage) |
| Payments | Razorpay Checkout SDK + Server-side Orders API |
| PDF Generation | HTML-to-print (iframe/window), QR codes via qrcode library |
| Excel/CSV | xlsx (SheetJS), papaparse |
| Voice Input | Web Speech API (SpeechRecognition) |
| Testing | Vitest, Testing Library, Playwright |
| Linting | ESLint with TypeScript + React Hooks plugins |
| Package Manager | Bun (bun.lockb) |
┌──────────────────────────────────────────────────────────┐
│ Browser (SPA) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ React + Vite + TypeScript + Tailwind + shadcn/ui │ │
│ │ │ │
│ │ ┌───────────┐ ┌──────────────┐ ┌───────────────┐ │ │
│ │ │ AuthCtx │→│ CompanyCtx │→│ LicenseCtx │ │ │
│ │ └───────────┘ └──────────────┘ └───────────────┘ │ │
│ │ │ │ │ │ │
│ │ └───────────────┼───────────────┘ │ │
│ │ ▼ │ │
│ │ ┌──────────────────┐ │ │
│ │ │ AppContext │ │ │
│ │ │ (All CRUD ops) │ │ │
│ │ └────────┬─────────┘ │ │
│ │ │ │ │
│ │ ┌────────┬──────────┼──────────┬────────────┐ │ │
│ │ │Pages │Components│ Hooks │ Utils │ │ │
│ │ │22 pages│20 comps │5 hooks │10 modules │ │ │
│ │ └────────┴──────────┴──────────┴────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────┼───────────┐ │
│ ▼ ▼ ▼ │
│ Supabase Client Razorpay SDK Web Speech API │
└──────────────┬───────────────────────────────────────────┘
│ HTTPS / WebSocket
▼
┌──────────────────────────────────────────────────────────┐
│ Supabase Platform │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL │ │ Auth (GoTrue)│ │ Edge Functions │ │
│ │ 19 tables │ │ JWT + OAuth │ │ 6 functions │ │
│ │ 22+ RLS │ │ │ │ - create-user │ │
│ │ policies │ │ │ │ - manage-user │ │
│ │ 16 functions│ │ │ │ - lookup-email │ │
│ │ 12 triggers │ │ │ │ - razorpay-* │ │
│ └──────────────┘ └──────────────┘ └──────────────────┘ │
│ ┌──────────────┐ │
│ │ Storage │ │
│ │ shared-pdfs │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Razorpay Payment Gateway │
│ Orders API → Checkout Modal → Webhook Verification │
└──────────────────────────────────────────────────────────┘
remix-of-lighting-fast-main/
├── public/
│ └── robots.txt
├── src/
│ ├── main.tsx # App entry point (React root)
│ ├── App.tsx # Router, providers, route definitions
│ ├── App.css # Global app styles
│ ├── index.css # Tailwind base + custom CSS variables
│ ├── vite-env.d.ts # Vite type declarations
│ ├── assets/ # Static assets
│ ├── components/
│ │ ├── AppLayout.tsx # Main shell (sidebar + header + content)
│ │ ├── AppSidebar.tsx # Navigation sidebar with permission filtering
│ │ ├── BulkActionBar.tsx # Sticky bar for bulk operations
│ │ ├── CompanySwitcher.tsx # Multi-company dropdown
│ │ ├── CouponsAdmin.tsx # Coupon CRUD management
│ │ ├── ExtraChargesEditor.tsx # Invoice extra charges/discounts
│ │ ├── InvoicePreviewDialog.tsx # Invoice PDF preview & print
│ │ ├── ItemPickerDialog.tsx # Multi-item selection for invoices
│ │ ├── NavLink.tsx # Active-aware navigation link
│ │ ├── PartyCombobox.tsx # Searchable party selector
│ │ ├── PaymentPreviewDialog.tsx # Payment receipt preview & print
│ │ ├── QuickAddItemDialog.tsx # Inline item creation from invoices
│ │ ├── QuickAddPartyDialog.tsx # Inline party creation from invoices
│ │ ├── SentReportsTab.tsx # Reports sent by current user
│ │ ├── SharedReportsTab.tsx # Reports received by current user
│ │ ├── ShareReportDialog.tsx # Share report with portal users
│ │ ├── SortHeader.tsx # Sortable table column header
│ │ ├── StatCard.tsx # Animated metric card
│ │ ├── UserMenu.tsx # User avatar/menu dropdown
│ │ ├── VoiceItemInput.tsx # Voice-based item entry
│ │ └── ui/ # shadcn/ui component library
│ ├── contexts/
│ │ ├── AppContext.tsx # Central data store (CRUD for all entities)
│ │ ├── AuthContext.tsx # Authentication state
│ │ ├── CompanyContext.tsx # Multi-company & permissions
│ │ └── LicenseContext.tsx # License/subscription state
│ ├── hooks/
│ │ ├── use-mobile.tsx # Responsive breakpoint (< 768px)
│ │ ├── use-persisted-columns.ts # Column visibility persistence
│ │ ├── use-row-selection.ts # Table row selection state
│ │ ├── use-show-inactive-items.ts # Toggle inactive items visibility
│ │ └── use-toast.ts # Toast notification system
│ ├── integrations/
│ │ └── supabase/
│ │ ├── client.ts # Supabase client singleton
│ │ └── types.ts # Auto-generated DB types
│ ├── lib/
│ │ └── utils.ts # cn() Tailwind merge utility
│ ├── pages/
│ │ ├── AdminPanel.tsx # Company member & settings management
│ │ ├── Auth.tsx # Login/signup page
│ │ ├── BackupRestore.tsx # Full data backup & restore
│ │ ├── Billing.tsx # License purchase & redemption
│ │ ├── CreateInvoice.tsx # Invoice creation (sale/purchase/returns)
│ │ ├── Dashboard.tsx # Business overview with stats
│ │ ├── EditInvoice.tsx # Edit existing invoice
│ │ ├── Index.tsx # Root redirect
│ │ ├── ItemHistory.tsx # Per-item transaction history
│ │ ├── Items.tsx # Product/service inventory management
│ │ ├── Landing.tsx # Public marketing page
│ │ ├── LicensesAdmin.tsx # Super admin license management
│ │ ├── NotFound.tsx # 404 page
│ │ ├── Parties.tsx # Customer/supplier management
│ │ ├── PartyLedger.tsx # Per-party debit/credit ledger
│ │ ├── PartyPortal.tsx # External party self-service portal
│ │ ├── PaymentIn.tsx # Payment received from customers
│ │ ├── PaymentOut.tsx # Payment made to suppliers
│ │ ├── PurchaseReturn.tsx # Purchase return (debit notes)
│ │ ├── Purchases.tsx # Purchase invoice list
│ │ ├── Reports.tsx # 14 report types with export
│ │ ├── SaleReturn.tsx # Sale return (credit notes)
│ │ ├── Sales.tsx # Sales invoice list
│ │ └── SettingsPage.tsx # Business profile configuration
│ ├── test/
│ │ ├── example.test.ts # Example test
│ │ └── setup.ts # Vitest setup
│ ├── types/
│ │ └── index.ts # TypeScript type definitions
│ └── utils/
│ ├── activityLog.ts # Audit trail logging
│ ├── gstExport.ts # GSTR-1/2/3B Excel export
│ ├── importExport.ts # Bulk import/export (Excel/CSV)
│ ├── invoiceCalc.ts # Invoice calculation engine
│ ├── invoicePdf.ts # Invoice HTML/PDF generation
│ ├── paymentPdf.ts # Payment receipt generation
│ ├── razorpay.ts # Razorpay SDK loader & launcher
│ ├── reportArtifacts.ts # Shareable report artifacts
│ ├── reportExport.ts # Report PDF/Excel export
│ └── reportGenerators.ts # Pure data computation for reports
├── supabase/
│ ├── config.toml # Supabase project configuration
│ ├── functions/
│ │ ├── create-user/index.ts # Create/invite company member
│ │ ├── lookup-email/index.ts # Phone-to-email lookup (login)
│ │ ├── manage-user/index.ts # Admin user update/delete
│ │ ├── razorpay-create-order/ # Create Razorpay payment order
│ │ ├── razorpay-verify-payment/ # Verify payment & issue license
│ │ └── razorpay-webhook/ # Razorpay async event handler
│ ├── migrations/ # 19 sequential SQL migrations
│ └── manual-migrations/ # 10 manual migration scripts
├── package.json
├── vite.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── vitest.config.ts
├── playwright.config.ts
├── eslint.config.js
├── postcss.config.js
├── components.json # shadcn/ui configuration
└── index.html # SPA entry HTML
# Clone the repository
git clone <repository-url>
cd remix-of-lighting-fast-main
# Install dependencies (using Bun)
bun install
# Or using npm
npm install
# Start dev server (runs on port 8080)
bun dev
# or
npm run dev
# Production build
bun run build
# Development build (with source maps)
bun run build:dev
bun run preview
Create a .env file in the project root:
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=eyJhbGciOiJIUzI1NiIs...
# Edge function environment variables (set in Supabase dashboard):
# SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs...
# RAZORPAY_KEY_ID=rzp_live_...
# RAZORPAY_KEY_SECRET=...
# RAZORPAY_WEBHOOK_SECRET=...
┌─────────────┐ ┌──────────────┐ ┌────────────────┐
│ Landing │ │ Auth Page │ │ Supabase │
│ Page (/) │───▶│ (/auth) │ │ Auth │
└─────────────┘ └──────┬───────┘ └───────┬────────┘
│ │
┌────────────┼────────────┐ │
▼ ▼ ▼ │
┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ Email │ │ Phone │ │ Sign Up │ │
│ Login │ │ Login │ │ (if │ │
└────┬─────┘ └────┬─────┘ │ enabled) │ │
│ │ └────┬─────┘ │
│ │ │ │
│ ┌──────▼──────┐ │ │
│ │ lookup-email│ │ │
│ │ Edge Fn │ │ │
│ │ (phone→email) │ │
│ └──────┬──────┘ │ │
│ │ │ │
▼ ▼ ▼ │
┌─────────────────────────────────────┐ │
│ supabase.auth.signInWithPassword │──┤
│ supabase.auth.signUp │ │
└─────────────────────────────────────┘ │
▼
┌──────────────┐
│ Triggers: │
│ 1. Profile │
│ 2. Role │
│ 3. Company │
│ 4. Trial │
│ License │
└──────┬───────┘
│
▼
┌──────────────┐
│ Dashboard / │
│ redirect │
└──────────────┘
On Sign Up (auth.users AFTER INSERT triggers):
1. handle_new_user() → Creates profile row
2. handle_new_user_role() → Assigns 'admin' (first user) or 'user' role
3. handle_new_user_company()→ Creates "Default Company" + owner membership
+ business_profiles row
4. create_trial_license() → Creates trial license (N days from app_settings)
┌────────────────────────────────────────────────────-───┐
│ User Logs In │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ CompanyContext │ │
│ │ fetchMemberships│ │
│ └────────┬────────┘ │
│ │ │
│ ┌──────────▼────────────┐ │
│ │ company_members │ │
│ │ JOIN companies │ │
│ │ WHERE user_id = me │ │
│ └──────────┬────────────┘ │
│ │ │
│ ┌─────────────▼──────────────┐ │
│ │ Returns: Company[] with │ │
│ │ role, page_permissions, │ │
│ │ can_edit_payments │ │
│ └─────────────┬──────────────┘ │
│ │ │
│ ┌────────────▼─────────────┐ │
│ │ Active company picked │ │
│ │ (localStorage persisted) │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ┌────────────▼─────────────┐ │
│ │ AppContext.fetchAll() │ │
│ │ Loads ALL data scoped │ │
│ │ to active company_id │ │
│ └──────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ CompanySwitcher UI │ │
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
│ │ │ Company A ✓ │ │ + Create New Company │ │ │
│ │ │ Company B │ │ (name → insert → │ │ │
│ │ │ Company C │ │ owner membership │ │ │
│ │ └──────────────┘ │ → business profile│ │ │
│ │ │ → trial license) │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────-─┘
Data Isolation (via RLS):
- All business tables have company_id column
- RLS policies enforce: is_company_member(company_id, auth.uid())
- Even if SQL is injected, PostgreSQL enforces isolation
┌──────────────────────────────────────────────────────────────-──┐
│ CreateInvoice Page │
│ │
│ URL: /sales/new?type=sale|purchase|sale_return|purchase_return │
│ │
│ ┌──────────────── Multi-Tab System ────────────────────┐ │
│ │ [Tab 1: INV-001] [Tab 2: INV-002] [+] │ │
│ │ │ │
│ │ ┌─── Party Selection ───────────────────────────┐ │ │
│ │ │ PartyCombobox (search by name/phone) │ │ │
│ │ │ [+ Quick Add Party] → QuickAddPartyDialog │ │ │
│ │ │ Auto-fills: state, GSTIN for GST calculation │ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─── Line Items ───────────────────────────────────┐│ │
│ │ │ [Add Item] → ItemPickerDialog (search, multi) ││ │
│ │ │ [Voice] → VoiceItemInput (speech-to-item) ││ │
│ │ │ [+ Quick] → QuickAddItemDialog (inline create) ││ │
│ │ │ ││ │
│ │ │ ┌───┬──────┬────┬─────┬──────┬────┬─────┬─────┐ ││ │
│ │ │ │Itm│ Qty │Unit│ MRP │ Rate │Disc│ GST │ Amt │ ││ │
│ │ │ ├───┼──────┼────┼─────┼──────┼────┼─────┼─────┤ ││ │
│ │ │ │...│ edit │ ...│ edit│ edit │edit│edit │edit │ ││ │
│ │ │ └───┴──────┴────┴─────┴──────┴────┴─────┴─────┘ ││ │
│ │ │ ││ │
│ │ │ Reverse Calculations: ││ │
│ │ │ Edit Amount → solveRateFromAmount() ││ │
│ │ │ Edit GST ₹ → solveGstRateFromGstAmount() ││ │
│ │ └──────────────────────────────────────────────────┘│ │
│ │ │ │
│ │ ┌─── Extra Charges/Discounts ──────────────────────┐│ │
│ │ │ ExtraChargesEditor ││ │
│ │ │ Charges: TCS, Freight, Packing (flat/%) ││ │
│ │ │ Discounts: Trade, Loyalty, Cash (flat/%) ││ │
│ │ └──────────────────────────────────────────────────┘│ │
│ │ │ │
│ │ ┌─── Totals ───────────────────────────────────────┐│ │
│ │ │ Subtotal: Σ line amounts ││ │
│ │ │ + Extra Charges / - Extra Discounts ││ │
│ │ │ GST Split: ││ │
│ │ │ Same State → CGST (50%) + SGST (50%) ││ │
│ │ │ Diff State → IGST (100%) ││ │
│ │ │ Round Off: auto (manual override) ││ │
│ │ │ Grand Total: computed ││ │
│ │ │ Payment Received: toggle + amount ││ │
│ │ └──────────────────────────────────────────────────┘│ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ On Save: │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 1. Insert invoice row (invoices table) │ │
│ │ 2. Insert invoice_items rows (bulk) │ │
│ │ 3. Adjust stock per line item: │ │
│ │ • sale/sale_return(credit): stock -= qty │ │
│ │ • purchase/purchase_return(debit): stock += qty │ │
│ │ 4. If payment received > 0: │ │
│ │ • Insert payment row │ │
│ │ • Update invoice status (paid/partial) │ │
│ │ 5. Log activity │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────┐
│ PaymentIn / PaymentOut Page │
│ │
│ ┌─ New Payment Dialog ──────────────────────┐ │
│ │ │ │
│ │ Party: [PartyCombobox] │ │
│ │ ├─ Auto-fills outstanding balance │ │
│ │ └─ Filters: customers (in) / │ │
│ │ suppliers (out) │ │
│ │ │ │
│ │ Amount: [₹ _____] (prefilled from │ │
│ │ outstanding) │ │
│ │ │ │
│ │ Mode: [Cash] [Bank] [UPI] [Cheque] │ │
│ │ │ │
│ │ Link Invoice: [Dropdown of unpaid │ │
│ │ invoices for party] │ │
│ │ │ │
│ │ Date: [DatePicker] │ │
│ │ Notes: [TextField] │ │
│ └───────────────────────────────────────────┘ │
│ │
│ On Save: │
│ ┌──────────────────────────────────────────┐ │
│ │ 1. Insert payment row │ │
│ │ 2. If linked to invoice: │ │
│ │ • invoice.amountPaid += payment.amount│ │
│ │ • invoice.status = │ │
│ │ amountPaid >= total ? 'paid' │ │
│ │ : amountPaid > 0 ? 'partial' │ │
│ │ : 'unpaid' │ │
│ │ 3. Log activity │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Outstanding Calculation (getPartyOutstanding): │
│ ┌───────────────────────────────────────────┐ │
│ │ openingBalance │ │
│ │ + Σ(sale invoices total) │ │
│ │ - Σ(purchase invoices total) │ │
│ │ - Σ(payments_in amount) │ │
│ │ + Σ(payments_out amount) │ │
│ │ + Σ(sale_return total) │ │
│ │ - Σ(purchase_return total) │ │
│ │ = Outstanding Balance │ │
│ └───────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ Billing Page │
│ │
│ ┌─── Current License Status ───────────────────────────┐ │
│ │ Type: Trial/Paid/Complimentary Status: Active │ │
│ │ Seats: 3 Expires: 2026-05-01 Days Left: 7 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Plans ────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌─ Monthly ─┐ ┌─ Lifetime ─┐ │ │
│ │ │ ₹199/mo │ │ ₹9,999 │ │ │
│ │ │ 3 seats │ │ one-time │ │ │
│ │ └─────┬─────┘ └─────┬──────┘ │ │
│ │ └───────┬───────┘ │ │
│ │ ▼ │ │
│ │ ┌─── Coupon (Optional) ──┐ │ │
│ │ │ Code: [______] [Apply] │ │ │
│ │ │ → validate_coupon RPC │ │ │
│ │ │ → Shows discount │ │ │
│ │ └────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─── Razorpay Checkout ─────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ 1. POST razorpay-create-order │ │ │
│ │ │ → Creates Razorpay order (amount in paise)│ │ │
│ │ │ → Inserts subscription_payments (created) │ │ │
│ │ │ │ │ │
│ │ │ 2. openRazorpay(options) │ │ │
│ │ │ → Razorpay modal (Card/UPI/Net Banking) │ │ │
│ │ │ │ │ │
│ │ │ 3. On Success: │ │ │
│ │ │ POST razorpay-verify-payment │ │ │
│ │ │ → HMAC-SHA256 signature verify │ │ │
│ │ │ → extend_or_create_paid_license RPC │ │ │
│ │ │ → Update payment status → 'paid' │ │ │
│ │ │ → Record coupon redemption │ │ │
│ │ │ │ │ │
│ │ │ 4. Webhook (backup): │ │ │
│ │ │ razorpay-webhook (async) │ │ │
│ │ │ → Handles payment.captured/failed/refunded│ │ │
│ │ └───────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌─── License Key Redemption ───────────────────────────┐ │
│ │ Key: [XXXX-XXXX-XXXX-XXXX] [Redeem] │ │
│ │ → redeem_license(key, company_id) RPC │ │
│ │ → Extends existing license or creates new one │ │
│ └──────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
License Lifecycle:
┌──────────┐ signup ┌───────┐ expires ┌─────────┐
│ No │ ─────────▶ │ Trial │ ────────▶ Expired │
│ License │ │ (N d) │ │ (locked)│
└──────────┘ └───┬───┘ └────┬────┘
│ pay │ pay/redeem
▼ ▼
┌─────────┐ ┌─────────┐
│ Paid │◀──────── │ Renewed │
│ (30d or │ extend │ │
│ lifetime)│ └─────────┘
└─────────┘
When license expires → all routes redirect to /billing (LicenseGate)
┌──────────────────────────────────────────────────────────┐
│ Reports Page │
│ │
│ ┌─── Report Selection ───────────────────────────────┐ │
│ │ 14 Report Types: │ │
│ │ │ │
│ │ Sales & Purchases: Profitability: │ │
│ │ • Sale Report • Profit & Loss │ │
│ │ • Purchase Report • Party-wise P&L │ │
│ │ • Day Book • Item-wise P&L │ │
│ │ • All Transactions │ │
│ │ Inventory: │ │
│ │ GST Returns: • Stock Summary │ │
│ │ • GSTR-1 • Stock Detail │ │
│ │ • GSTR-2 │ │
│ │ • GSTR-3B Party: │ │
│ │ • Party Statement │ │
│ │ • All Parties Report │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Filters ────────────────────────────────────────┐ │
│ │ From: [Date] To: [Date] Party: [PartyCombobox] │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Actions ────────────────────────────────────────┐ │
│ │ [Export PDF] → Opens HTML in new tab → window.print│ │
│ │ [Export Excel] → XLSX.writeFile download │ │
│ │ [Share] → ShareReportDialog │ │
│ │ ├─ Select party-portal recipients │ │
│ │ ├─ buildReportArtifact(HTML + XLSX64) │ │
│ │ └─ Insert into shared_reports table │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Tabs ───────────────────────────────────────────┐ │
│ │ [Generate] [Sent Reports] [Shared With Me] │ │
│ │ │ │
│ │ SentReportsTab: │ │
│ │ Shows reports sent by me, view/download status │ │
│ │ │ │
│ │ SharedReportsTab: │ │
│ │ Shows reports received, auto-marks viewed │ │
│ └────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
GSTR Export Details:
GSTR-1 (Sales):
Sheets: B2B, B2CL (>₹2.5L inter-state), B2CS, CDNR, HSN
GSTR-2 (Purchases):
Sheets: B2B, CDNR
GSTR-3B (Summary):
Sheets: 3.1 Outward Supplies, 4. ITC, 6.1 Tax Payment
┌───────────────────────────────────────────────────────────┐
│ Party Portal │
│ │
│ ┌─── Access Model ───────────────────────────────────┐ │
│ │ │ │
│ │ Admin creates user with role='party' │ │
│ │ ↓ │ │
│ │ Admin links user to a party record │ │
│ │ (party_access table): │ │
│ │ • party_user_id → auth user │ │
│ │ • party_id → parties.id │ │
│ │ • can_view_sales: true/false │ │
│ │ • can_view_purchases: true/false │ │
│ │ • can_view_payments: true/false │ │
│ │ ↓ │ │
│ │ Party user logs in → isParty flag set │ │
│ │ ↓ │ │
│ │ All routes redirect to /portal │ │
│ │ (No license check — they're not paying users) │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─── Portal Tabs ────────────────────────────────────┐ │
│ │ [Ledger] [Sales] [Purchases] [Payments] [Reports] │ │
│ │ │ │
│ │ Each tab: │ │
│ │ • Filters to linked party only │ │
│ │ • Respects can_view_* permissions │ │
│ │ • RLS enforces at DB level via party_access │ │
│ │ • Search, sort, export PDF/Excel │ │
│ │ • Shared Reports tab shows received reports │ │
│ └────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ Backup & Restore Page │
│ │
│ ┌─── Export (.bkp) ───────────────────────────────┐ │
│ │ 1. Fetch all company data from Supabase: │ │
│ │ parties, items, invoices, invoice_items, │ │
│ │ payments, business_profiles, custom_units, │ │
│ │ categories, activity_log, shared_reports, │ │
│ │ party_access │ │
│ │ 2. Serialize to JSON │ │
│ │ 3. Download as .bkp file │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─── Import (.bkp) ───────────────────────────────┐ │
│ │ 1. Parse .bkp JSON file │ │
│ │ 2. Purge existing data (FK-safe order): │ │
│ │ a. NULL linked_invoice_id on invoices │ │
│ │ b. Delete: payments → invoice_items → │ │
│ │ invoices → party_access → items → │ │
│ │ parties → activity_log → │ │
│ │ shared_reports → custom_units → │ │
│ │ categories → business_profiles │ │
│ │ 3. Upsert in batches of 500: │ │
│ │ Reverse order (business_profiles first, │ │
│ │ then parties, items, invoices, etc.) │ │
│ │ 4. Verify record counts │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─── Delete All ──────────────────────────────────┐ │
│ │ Purges all company data with confirmation │ │
│ └─────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
│ Admin Panel │
│ │
│ ┌─── Members Tab ───────────────────────────────────┐ │
│ │ │ │
│ │ [+ Add Member] │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Email: user@example.com │ │ │
│ │ │ Password: (for new users) │ │ │
│ │ │ Display Name: John │ │ │
│ │ │ Role: [owner|admin|staff|member|party] │ │ │
│ │ │ │ │ │
│ │ │ → Calls create-user Edge Function │ │ │
│ │ │ • Checks seat limit from license │ │ │
│ │ │ • Creates auth user (if new) │ │ │
│ │ │ • Inserts company_members │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Members List: │ │
│ │ ┌────────────┬──────┬────────────────────────┐ │ │
│ │ │ Name/Email │ Role │ Actions │ │ │
│ │ ├────────────┼──────┼────────────────────────┤ │ │
│ │ │ Alice │owner │ (no actions on self) │ │ │
│ │ │ Bob │admin │ [Edit] [Permissions] │ │ │
│ │ │ Carol │staff │ [Edit] [Permissions] │ │ │
│ │ │ Dave │party │ [Link Party] [Remove] │ │ │
│ │ └────────────┴──────┴────────────────────────┘ │ │
│ │ │ │
│ │ Page Permissions Matrix (per member): │ │
│ │ ┌──────────┬──────┬────────┬──────┬─────────┐ │ │
│ │ │ Page │ View │ Create │ Edit │ Delete │ │ │
│ │ ├──────────┼──────┼────────┼──────┼─────────┤ │ │
│ │ │ Dashboard│ ✓ │ - │ - │ - │ │ │
│ │ │ Sales │ ✓ │ ✓ │ ✓ │ ✗ │ │ │
│ │ │ Items │ ✓ │ ✓ │ ✗ │ ✗ │ │ │
│ │ │ ... │ │ │ │ │ │ │
│ │ └──────────┴──────┴────────┴──────┴─────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌─── Activity Log Tab ─────────────────────────────┐ │
│ │ [2026-04-24 10:30] Alice created Invoice INV-042 │ │
│ │ [2026-04-24 09:15] Bob edited Party "XYZ Corp" │ │
│ │ [2026-04-23 18:00] Carol deleted Payment PAY-007 │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌─── Settings Tab ─────────────────────────────────┐ │
│ │ Allow New Signups: [Toggle] │ │
│ │ (Stored in app_settings.allow_signups) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ auth.users (Supabase) │
│ │ PK: id │
└──────────────────────────────┬──────────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ profiles │ │ user_roles │ │ super_admins │
│ (1:1) │ │ (1:N) │ │ (email PK) │
└──────────────┘ └──────────────┘ └──────────────────┘
│
│ user_id
▼
┌──────────────┐ ┌────────────────────┐
│ companies │◄───────│ company_members │
│ PK: id │ 1:N │ (user_id, │
│ owner_id ───┘ │ company_id, │
└──────┬───────┘ │ role, │
│ │ page_permissions)│
│ └────────────────────┘
│
┌────┼────┬───────────────┬───────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐┌────────┐ ┌──────────────┐ ┌─────────────┐
│business││parties │ │ items │ │ payments │
│profiles││ │ │ │ │ │
└────────┘└───┬────┘ └──────────────┘ └─────────────┘
│ │ │
│ │ │
▼ ▼ │
┌──────────────┐ ┌──────────────┐ │
│ party_access │ │ invoices │◄─────┘
│(portal perms)│ │ (sale/ │ linked
└──────────────┘ │ purchase/ │
│ returns) │
└──────┬───────┘
│
▼
┌──────────────┐
│invoice_items │
│(line items) │
└──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ custom_units │ │ categories │ │ activity_log │
│ (per company)│ │(per company) │ │ (audit trail)│
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐
│shared_reports│
│(sender→recip)│
└──────────────┘
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ licenses │ │subscription_ │ │coupon_redemptions│
│ (per company)│ │ payments │ │ │
└──────────────┘ └──────────────────┘ └──────────────────┘
│
▼
┌──────────────┐
│ coupons │
└──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ app_settings │ │staff_perms │ │page_perms │
│ (global KV) │ │(legacy) │ │(legacy) │
└──────────────┘ └──────────────┘ └──────────────┘
profilesUser profile data, auto-created on signup.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK, gen_random_uuid() |
user_id |
uuid |
NOT NULL, UNIQUE, FK → auth.users(id) CASCADE |
display_name |
text |
|
avatar_url |
text |
|
phone |
text |
|
email |
text |
|
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
user_rolesGlobal application roles.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
role |
app_role |
NOT NULL, DEFAULT 'user' |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
UNIQUE(user_id, role) |
companiesMulti-tenant company records.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
name |
text |
NOT NULL |
owner_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
company_membersCompany membership with roles and permissions.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
role |
company_role |
NOT NULL, DEFAULT 'member' |
can_edit_payments |
boolean |
NOT NULL, DEFAULT false |
page_permissions |
jsonb |
NOT NULL, DEFAULT '{}' |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
UNIQUE(company_id, user_id) |
page_permissions JSON structure:
{
"dashboard": { "view": true },
"parties": { "view": true, "create": true, "edit": true, "delete": false },
"items": { "view": true, "create": true, "edit": false, "delete": false },
"sales": { "view": true, "create": true, "edit": true, "delete": false },
"purchases": { "view": true, "create": true, "edit": true, "delete": false },
"payment_in": { "view": true, "create": true, "edit": false, "delete": false },
"payment_out": { "view": true, "create": true, "edit": false, "delete": false },
"sale_return": { "view": true, "create": true },
"purchase_return": { "view": true, "create": true },
"reports": { "view": true },
"settings": { "view": true, "edit": true }
}
business_profilesBusiness configuration per company.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
NOT NULL, UNIQUE, FK → companies(id) CASCADE |
name |
text |
NOT NULL, DEFAULT 'My Business' |
gstin |
text |
DEFAULT '' |
phone |
text |
DEFAULT '' |
email |
text |
DEFAULT '' |
address |
text |
DEFAULT '' |
state |
text |
DEFAULT 'Maharashtra' |
upi_id |
text |
DEFAULT '' |
logo_url |
text |
|
default_invoice_template |
text |
DEFAULT 'a5' |
invoice_panel_style |
text |
NOT NULL, DEFAULT 'classic' |
whatsapp_phone |
text |
|
whatsapp_auto_share_sales |
boolean |
NOT NULL, DEFAULT false |
whatsapp_auto_share_purchases |
boolean |
NOT NULL, DEFAULT false |
whatsapp_auto_share_payments |
boolean |
NOT NULL, DEFAULT false |
whatsapp_auto_share_outstanding |
boolean |
NOT NULL, DEFAULT false |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
partiesCustomer and supplier records.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
name |
text |
NOT NULL |
phone |
text |
DEFAULT '' |
email |
text |
DEFAULT '' |
gstin |
text |
DEFAULT '' |
gst_type |
text |
DEFAULT 'unregistered' |
state |
text |
DEFAULT '' |
billing_address |
text |
DEFAULT '' |
shipping_address |
text |
DEFAULT '' |
type |
text |
NOT NULL, DEFAULT 'customer' |
opening_balance |
numeric |
DEFAULT 0 |
opening_balance_date |
text |
DEFAULT '' |
opening_balance_type |
text |
DEFAULT 'receive' |
credit_limit |
numeric |
DEFAULT 0 |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
itemsProduct and service catalog.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
name |
text |
NOT NULL |
item_type |
text |
DEFAULT 'product' |
item_code |
text |
DEFAULT '' |
hsn_code |
text |
DEFAULT '' |
category |
text |
DEFAULT '' |
primary_unit |
text |
DEFAULT 'pcs' |
secondary_unit |
text |
DEFAULT '' |
conversion_rate |
numeric |
DEFAULT 1 |
sale_price |
numeric |
DEFAULT 0 |
sale_price_secondary |
numeric |
DEFAULT 0 |
sale_price_with_tax |
boolean |
DEFAULT false |
purchase_price |
numeric |
DEFAULT 0 |
purchase_price_secondary |
numeric |
DEFAULT 0 |
purchase_price_with_tax |
boolean |
DEFAULT false |
discount_value |
numeric |
DEFAULT 0 |
discount_type |
text |
DEFAULT 'percentage' |
gst_rate |
numeric |
DEFAULT 18 |
opening_stock |
numeric |
DEFAULT 0 |
stock_as_of_date |
text |
DEFAULT '' |
stock_price_per_unit |
numeric |
DEFAULT 0 |
min_stock_qty |
numeric |
DEFAULT 0 |
item_location |
text |
DEFAULT '' |
stock |
numeric |
DEFAULT 0 |
mrp |
numeric |
DEFAULT 0 |
is_active |
boolean |
NOT NULL, DEFAULT true |
image_url |
text |
|
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
invoicesAll invoice types (sale, purchase, sale_return, purchase_return).
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
invoice_number |
text |
NOT NULL |
date |
text |
NOT NULL |
party_id |
text |
NOT NULL |
party_name |
text |
NOT NULL |
party_state |
text |
DEFAULT '' |
party_gstin |
text |
DEFAULT '' |
subtotal |
numeric |
DEFAULT 0 |
cgst |
numeric |
DEFAULT 0 |
sgst |
numeric |
DEFAULT 0 |
igst |
numeric |
DEFAULT 0 |
total |
numeric |
DEFAULT 0 |
notes |
text |
DEFAULT '' |
type |
text |
NOT NULL (sale, purchase, sale_return, purchase_return) |
status |
text |
DEFAULT 'unpaid' (paid, unpaid, partial) |
amount_paid |
numeric |
DEFAULT 0 |
received_amount |
numeric |
DEFAULT 0 |
linked_invoice_id |
text |
(for returns → original invoice) |
extra_charges |
jsonb |
NOT NULL, DEFAULT '[]' |
extra_discounts |
jsonb |
NOT NULL, DEFAULT '[]' |
round_off |
numeric |
NOT NULL, DEFAULT 0 |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
extra_charges / extra_discounts JSON structure (array of ExtraLine):
[
{
"id": "uuid",
"label": "Freight",
"amount": 500,
"amountType": "flat",
"taxable": false,
"gstRate": 0,
"applyStage": "post"
}
]
invoice_itemsLine items within an invoice.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
invoice_id |
uuid |
NOT NULL, FK → invoices(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
item_id |
text |
NOT NULL |
item_name |
text |
NOT NULL |
hsn_code |
text |
DEFAULT '' |
unit |
text |
DEFAULT 'pcs' |
qty |
numeric |
DEFAULT 1 |
mrp |
numeric |
DEFAULT 0 |
rate |
numeric |
DEFAULT 0 |
price_with_tax |
boolean |
DEFAULT false |
discount |
numeric |
DEFAULT 0 |
discount_type |
text |
DEFAULT 'percentage' |
gst_rate |
numeric |
DEFAULT 0 |
gst_amount |
numeric |
DEFAULT 0 |
amount |
numeric |
DEFAULT 0 |
paymentsPayment-in (from customers) and payment-out (to suppliers).
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
date |
text |
NOT NULL |
party_id |
text |
NOT NULL |
party_name |
text |
NOT NULL |
invoice_id |
text |
(linked invoice) |
invoice_number |
text |
|
amount |
numeric |
DEFAULT 0 |
mode |
text |
DEFAULT 'cash' (cash, bank, upi, cheque) |
type |
text |
NOT NULL (in, out) |
notes |
text |
DEFAULT '' |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
party_accessControls which party users can view what data in the Party Portal.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
party_user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
party_id |
text |
NOT NULL |
can_view_sales |
boolean |
DEFAULT false |
can_view_purchases |
boolean |
DEFAULT false |
can_view_payments |
boolean |
DEFAULT false |
granted_by |
uuid |
FK → auth.users(id) |
company_id |
uuid |
FK → companies(id) CASCADE |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
UNIQUE(party_user_id, party_id) |
custom_unitsUser-defined measurement units.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
unit_name |
text |
NOT NULL |
UNIQUE(company_id, unit_name) |
categoriesItem categories.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
name |
text |
NOT NULL |
UNIQUE(company_id, name) |
activity_logAudit trail for all create/edit/delete operations.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
FK → auth.users(id) SET NULL |
user_name |
text |
|
action |
text |
NOT NULL, CHECK IN (create, edit, delete) |
entity_type |
text |
NOT NULL |
entity_id |
text |
|
entity_description |
text |
|
details |
jsonb |
DEFAULT '{}' |
company_id |
uuid |
FK → companies(id) CASCADE |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
shared_reportsReports shared between users via the portal.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
shared_by |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
recipient_user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
report_key |
text |
NOT NULL |
report_label |
text |
NOT NULL |
title |
text |
NOT NULL |
from_date |
text |
NOT NULL |
to_date |
text |
NOT NULL |
party_id |
uuid |
|
file_name |
text |
NOT NULL |
html_content |
text |
NOT NULL |
xlsx_base64 |
text |
NOT NULL |
message |
text |
|
company_id |
uuid |
FK → companies(id) CASCADE |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
viewed_at |
timestamptz |
|
downloaded_at |
timestamptz |
page_permissions (Legacy)Per-user page-level access control. Superseded by company_members.page_permissions JSON.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
page_key |
text |
NOT NULL |
can_view |
boolean |
NOT NULL, DEFAULT false |
can_create |
boolean |
NOT NULL, DEFAULT false |
can_edit |
boolean |
NOT NULL, DEFAULT false |
can_delete |
boolean |
NOT NULL, DEFAULT false |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
UNIQUE(user_id, page_key) |
staff_permissions (Legacy)Staff payment edit permissions. Superseded by company_members.can_edit_payments.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, UNIQUE, FK → auth.users(id) CASCADE |
can_edit_payments |
boolean |
NOT NULL, DEFAULT false |
granted_by |
uuid |
FK → auth.users(id) |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
app_settingsGlobal key-value application settings.
| Column | Type | Constraints |
|---|---|---|
setting_key |
text |
PK |
setting_value |
text |
NOT NULL |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
Seeded values:
| Key | Default Value | Description |
|—–|————–|————-|
| allow_signups | 'true' | Whether new user registration is enabled |
| trial_days | '7' | Default trial license duration in days |
super_adminsPlatform super-administrators.
| Column | Type | Constraints |
|---|---|---|
email |
text |
PK |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
Seeded: admin@admin.com
licensesPer-company license records.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
company_id |
uuid |
FK → companies(id) CASCADE (nullable) |
license_key |
text |
UNIQUE |
type |
license_type |
NOT NULL, DEFAULT 'trial' |
status |
license_status |
NOT NULL, DEFAULT 'active' |
seats |
integer |
NOT NULL, DEFAULT 3 |
starts_at |
timestamptz |
NOT NULL, DEFAULT now() |
expires_at |
timestamptz |
NOT NULL |
notes |
text |
|
issued_by |
uuid |
FK → auth.users(id) SET NULL |
issued_to_email |
text |
|
redeemed_at |
timestamptz |
|
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
couponsDiscount coupons for subscription payments.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
code |
text |
UNIQUE, NOT NULL |
type |
coupon_type |
NOT NULL (percent, flat) |
value |
numeric(10,2) |
NOT NULL, CHECK > 0 |
applies_to |
coupon_applies_to |
NOT NULL, DEFAULT 'both' |
max_uses |
integer |
(NULL = unlimited) |
used_count |
integer |
NOT NULL, DEFAULT 0 |
expires_at |
timestamptz |
|
active |
boolean |
NOT NULL, DEFAULT true |
notes |
text |
|
created_by |
uuid |
FK → auth.users(id) SET NULL |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
subscription_paymentsRazorpay payment records for license purchases.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
company_id |
uuid |
FK → companies(id) SET NULL |
plan |
plan_code |
NOT NULL (monthly, lifetime) |
amount |
numeric(10,2) |
NOT NULL |
base_amount |
numeric(10,2) |
NOT NULL |
discount_amount |
numeric(10,2) |
NOT NULL, DEFAULT 0 |
coupon_id |
uuid |
FK → coupons(id) SET NULL |
coupon_code |
text |
|
razorpay_order_id |
text |
UNIQUE |
razorpay_payment_id |
text |
|
razorpay_signature |
text |
|
status |
payment_status |
NOT NULL, DEFAULT 'created' |
license_id |
uuid |
FK → licenses(id) SET NULL |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
paid_at |
timestamptz |
coupon_redemptionsTracks which users have used which coupons.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
coupon_id |
uuid |
NOT NULL, FK → coupons(id) CASCADE |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
payment_id |
uuid |
FK → subscription_payments(id) SET NULL |
redeemed_at |
timestamptz |
NOT NULL, DEFAULT now() |
UNIQUE(coupon_id, user_id) |
| Enum | Values | Usage |
|---|---|---|
app_role |
admin, staff, user, party |
Global user roles |
company_role |
owner, admin, staff, member, party |
Per-company roles |
license_type |
trial, paid, complimentary |
License classification |
license_status |
active, expired, revoked |
License state |
plan_code |
monthly, lifetime |
Subscription plan |
coupon_type |
percent, flat |
Discount calculation mode |
coupon_applies_to |
monthly, lifetime, both |
Plan-specific coupons |
payment_status |
created, paid, failed, refunded |
Razorpay payment state |
company_active_licenseReturns the most recently expiring active license per company.
SELECT company_id, id, type, status, seats, starts_at, expires_at, license_key, notes
FROM licenses
WHERE status = 'active'
ORDER BY expires_at DESC
LIMIT 1 per company (DISTINCT ON company_id)
Granted SELECT to authenticated role.
| Function | Signature | Description |
|---|---|---|
has_role |
(uuid, app_role) → boolean |
SECURITY DEFINER. Checks global user_roles table |
get_user_role |
(uuid) → app_role |
Returns highest-priority app role |
is_company_member |
(company_id uuid, user_id uuid) → boolean |
SECURITY DEFINER. Checks company_members |
has_company_role |
(company_id uuid, user_id uuid, roles company_role[]) → boolean |
SECURITY DEFINER. Checks for specific roles |
is_company_admin |
(company_id uuid, user_id uuid) → boolean |
SECURITY DEFINER. Checks for owner/admin |
can_write_company |
(company_id uuid, user_id uuid) → boolean |
SECURITY DEFINER. Owner/admin/staff/member (not party) |
is_company_party |
(company_id uuid, user_id uuid) → boolean |
SECURITY DEFINER. Checks party role |
is_super_admin |
() → boolean |
SECURITY DEFINER. JWT email in super_admins |
handle_new_user |
() → trigger |
Creates profile row on signup |
handle_new_user_role |
() → trigger |
First user → admin, rest → user role |
handle_new_user_company |
() → trigger |
Creates “Default Company” + membership + profile |
update_updated_at_column |
() → trigger |
Sets updated_at = now() on UPDATE |
create_trial_license |
() → trigger |
Creates trial license when company is created |
redeem_license |
(text, uuid) → licenses |
Redeems a license key for a company |
extend_or_create_paid_license |
(uuid, int, text, text, text, boolean) → licenses |
Extends or creates paid license |
validate_coupon |
(text, plan_code, numeric) → jsonb |
Validates coupon and returns discount |
| Trigger | Table | Event | Function |
|---|---|---|---|
on_auth_user_created |
auth.users |
AFTER INSERT | handle_new_user() |
on_auth_user_created_role |
auth.users |
AFTER INSERT | handle_new_user_role() |
on_auth_user_created_company |
auth.users |
AFTER INSERT | handle_new_user_company() |
update_profiles_updated_at |
profiles |
BEFORE UPDATE | update_updated_at_column() |
update_party_access_updated_at |
party_access |
BEFORE UPDATE | update_updated_at_column() |
update_staff_permissions_updated_at |
staff_permissions |
BEFORE UPDATE | update_updated_at_column() |
update_business_profiles_updated_at |
business_profiles |
BEFORE UPDATE | update_updated_at_column() |
update_parties_updated_at |
parties |
BEFORE UPDATE | update_updated_at_column() |
update_items_updated_at |
items |
BEFORE UPDATE | update_updated_at_column() |
update_invoices_updated_at |
invoices |
BEFORE UPDATE | update_updated_at_column() |
update_payments_updated_at |
payments |
BEFORE UPDATE | update_updated_at_column() |
update_companies_updated_at |
companies |
BEFORE UPDATE | update_updated_at_column() |
update_company_members_updated_at |
company_members |
BEFORE UPDATE | update_updated_at_column() |
trg_create_trial_license |
companies |
AFTER INSERT | create_trial_license() |
| Index | Table | Column(s) |
|---|---|---|
idx_parties_user_id |
parties | user_id |
idx_parties_type |
parties | type |
idx_items_user_id |
items | user_id |
idx_invoices_user_id |
invoices | user_id |
idx_invoices_party_id |
invoices | party_id |
idx_invoices_type |
invoices | type |
idx_invoice_items_invoice_id |
invoice_items | invoice_id |
idx_payments_user_id |
payments | user_id |
idx_payments_party_id |
payments | party_id |
items_is_active_idx |
items | is_active |
shared_reports_recipient_idx |
shared_reports | (recipient_user_id, created_at DESC) |
shared_reports_shared_by_idx |
shared_reports | (shared_by, created_at DESC) |
companies_owner_id_idx |
companies | owner_id |
company_members_user_id_idx |
company_members | user_id |
company_members_company_id_idx |
company_members | company_id |
business_profiles_company_id_idx |
business_profiles | company_id |
parties_company_id_idx |
parties | company_id |
items_company_id_idx |
items | company_id |
invoices_company_id_idx |
invoices | company_id |
invoice_items_company_id_idx |
invoice_items | company_id |
payments_company_id_idx |
payments | company_id |
custom_units_company_id_idx |
custom_units | company_id |
categories_company_id_idx |
categories | company_id |
activity_log_company_id_idx |
activity_log | company_id |
shared_reports_company_id_idx |
shared_reports | company_id |
party_access_company_id_idx |
party_access | company_id |
idx_licenses_company |
licenses | company_id |
idx_licenses_status |
licenses | status |
idx_licenses_key |
licenses | license_key |
idx_coupons_code |
coupons | lower(code) |
idx_subscription_payments_user |
subscription_payments | user_id |
idx_subscription_payments_order |
subscription_payments | razorpay_order_id |
All tables have RLS enabled. Policies use SECURITY DEFINER helper functions for role checks.
| Operation | Who | Logic |
|---|---|---|
| SELECT | Company members | is_company_member(company_id, auth.uid()) |
| INSERT | Writers | can_write_company(company_id, auth.uid()) — owner/admin/staff/member |
| UPDATE | Writers | can_write_company(company_id, auth.uid()) |
| DELETE | Admins only | is_company_admin(company_id, auth.uid()) |
party_access JOIN)| Table | Operation | Logic |
|---|---|---|
invoices |
SELECT | Party user can view invoices linked to their party (sales if can_view_sales, purchases if can_view_purchases) |
invoice_items |
SELECT | Via invoice JOIN |
payments |
SELECT | Party user can view payments if can_view_payments |
| Table | Read | Write |
|---|---|---|
licenses |
Company members + super admins | Super admins only |
coupons |
All authenticated users | Super admins only |
subscription_payments |
Own user + super admins | (Created via edge functions) |
coupon_redemptions |
Own user + super admins | (Created via edge functions) |
| Table | Policy |
|---|---|
super_admins |
SELECT: lower(email) = jwt_email (self-check only) |
app_settings |
SELECT: public read; WRITE: has_role('admin') |
companies |
SELECT: members + super admins; INSERT: own; UPDATE: admin/owner; DELETE: owner |
| Bucket | Public | Purpose |
|---|---|---|
shared-pdfs |
Yes | WhatsApp-shareable invoice/report PDFs |
Policies:
bucket_id = 'shared-pdfs'bucket_id = 'shared-pdfs' AND auth.uid() = folder owner| # | Date | Migration | Description |
|---|---|---|---|
| 001 | 2026-04-04 | Initial schema | profiles, user_roles, party_access, staff_permissions, activity_log + app_role enum + role functions + RLS + triggers |
| 002 | 2026-04-04 | Auto-admin role | handle_new_user_role() — first user → admin, rest → user |
| 003 | 2026-04-06 | Business schema | business_profiles, parties, items, invoices, invoice_items, payments, custom_units, categories, page_permissions, app_settings + party portal RLS |
| 004 | 2026-04-07 | Schema refinement | Re-creates business tables with updated_at, granular RLS, indexes |
| 005 | 2026-04-10 | MRP column | Adds mrp to items |
| 006 | 2026-04-12 | (Empty) | Intentionally blank migration |
| 007 | 2026-04-16 | Logo URL | Adds logo_url to business_profiles |
| 008 | 2026-04-17 | Admin policies | Admin DELETE on invoices and payments |
| 009 | — | Fix migration | business_profiles unique, default_invoice_template, idempotent DDL fixes |
| 010 | — | Extra charges | extra_charges, extra_discounts, round_off on invoices; WhatsApp settings; shared-pdfs bucket |
| 011 | — | Active flag | is_active boolean on items with index |
| 012 | — | Shared reports | shared_reports table with sender/recipient RLS |
| 013 | — | Multi-company | companies, company_members, company_role enum, security helpers, company_id on all tables, backfill |
| 014 | — | RLS rewrite | All old RLS replaced with company-membership-based policies |
| 015 | — | Backup columns | invoice_panel_style, image_url |
| 016 | — | Licensing | super_admins, licenses, enums, company_active_license view, redeem_license(), trial trigger |
| 017 | — | Razorpay + Coupons | coupons, subscription_payments, coupon_redemptions, validate_coupon() |
| 018 | — | Nullable company | licenses.company_id nullable for pre-assigned keys |
| 019 | — | Super admin view | SELECT policy on companies for super admins |
| 020 | — | License extension | extend_or_create_paid_license(), rewritten redeem_license() |
| 021 | — | Unique key fix | Clears license_key before extending to avoid constraint violation |
| 022 | — | Key/email propagate | Updated extension function to prefer newest key/email |
<QueryClientProvider>
<TooltipProvider>
<BrowserRouter>
<AuthProvider> ← Supabase Auth, user state, profile
<CompanyProvider> ← Multi-company memberships, roles, permissions
<LicenseProvider> ← License state, super admin check
<AppRoutes /> ← Route definitions + guards
<AppProvider> ← All business data CRUD (scoped to active company)
<AppLayout> ← Sidebar, header, content area
{page}
</AppLayout>
</AppProvider>
</LicenseProvider>
</CompanyProvider>
</AuthProvider>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
Unauthenticated routes:
| Path | Page | Access |
|——|——|——–|
| / | Landing | Public marketing page |
| /auth | Auth | Login/signup |
| * | → /auth | Redirect with return URL |
Party user routes (role = party):
| Path | Page | Access |
|——|——|——–|
| /portal | PartyPortal | Party users only |
| * | → /portal | Redirect |
Authenticated user routes (license-gated):
| Path | Page | Guard |
|——|——|——-|
| / | Dashboard | dashboard |
| /parties | Parties | parties |
| /items | Items | items |
| /sales | Sales | sales |
| /purchases | Purchases | purchases |
| /sales/new | CreateInvoice | sales |
| /sales/edit/:id | EditInvoice | sales |
| /party-ledger/:id | PartyLedger | parties |
| /item-history/:id | ItemHistory | items |
| /payment-in | PaymentIn | payment_in |
| /payment-out | PaymentOut | payment_out |
| /sale-return | SaleReturn | sale_return |
| /purchase-return | PurchaseReturn | purchase_return |
| /reports | Reports | reports |
| /settings | SettingsPage | settings |
| /backup | BackupRestore | settings |
| /admin | AdminPanel | (unrestricted for logged-in) |
| /admin/licenses | LicensesAdmin | (super admin redirect) |
| /billing | Billing | (always accessible, even expired) |
| /portal | PartyPortal | (any authenticated user) |
LicenseGate: When license is expired/missing, all routes except /billing redirect to the billing page.
| Page | Purpose | Key Features |
|---|---|---|
| Dashboard | Business overview | Sales/purchase/payment totals, outstanding, global search, recent transactions, quick actions |
| Parties | Customer/supplier CRUD | Search, filter by type, bulk operations, import/export Excel/CSV, GSTIN validation |
| Items | Product/service inventory | Category management, dual units, stock tracking, inactive toggle, bulk ops, import/export |
| Sales | Sale invoice list | Status filter, date range, sortable columns, PDF preview, bulk ops, import/export |
| Purchases | Purchase invoice list | Same as Sales for purchase type |
| CreateInvoice | Multi-tab invoice creator | Party search + quick-add, item picker + voice input + quick-add, reverse calculations, extra charges, GST split, payment recording |
| EditInvoice | Edit existing invoice | Same as CreateInvoice, loads from existing data |
| PaymentIn | Customer payment recording | Auto-fill from outstanding, link to invoice, receipt preview |
| PaymentOut | Supplier payment recording | Same as PaymentIn for outgoing payments |
| SaleReturn | Credit note list | Create/list/preview sale returns |
| PurchaseReturn | Debit note list | Create/list/preview purchase returns |
| Reports | 14 report types | PDF/Excel export, share to portal, GSTR-1/2/3B |
| PartyLedger | Per-party ledger | Running balance, debit/credit entries, export |
| ItemHistory | Per-item transactions | Stock movements, sale/purchase totals, export |
| SettingsPage | Business configuration | Logo, GSTIN, state, invoice template, UPI ID |
| AdminPanel | Team management | Member CRUD, roles, page permissions, activity log, app settings |
| Billing | License management | Plans, Razorpay payment, coupon apply, key redemption |
| LicensesAdmin | Super admin licenses | Generate keys, manage licenses, coupons admin, payment toggle |
| BackupRestore | Data backup/restore | Export/import .bkp files, delete all |
| PartyPortal | External party portal | Filtered view of invoices, payments, shared reports |
| Auth | Login/signup | Email/phone login, signup with toggle |
| Landing | Marketing page | Hero, features, pricing (₹0/₹199/₹9,999), CTA |
| Component | Purpose |
|---|---|
| AppLayout | Shell: sidebar + header (help, quick create, user menu) + content |
| AppSidebar | Navigation: permission-filtered menu, company switcher, trial badge |
| BulkActionBar | Sticky bar for bulk edit/delete when rows selected |
| CompanySwitcher | Dropdown to switch/create companies |
| CouponsAdmin | Coupon CRUD (embedded in LicensesAdmin) |
| ExtraChargesEditor | Invoice extra charges/discounts tabs |
| InvoicePreviewDialog | Multi-format invoice PDF preview + print |
| ItemPickerDialog | Multi-item selection with qty for invoices |
| NavLink | Active-aware navigation link wrapper |
| PartyCombobox | Searchable party dropdown (name + phone) |
| PaymentPreviewDialog | Payment receipt preview + print |
| QuickAddItemDialog | Full item creation dialog from within invoice |
| QuickAddPartyDialog | Quick party creation from within invoice/payment |
| SentReportsTab | Reports sent by current user |
| SharedReportsTab | Reports received by current user |
| ShareReportDialog | Multi-recipient report sharing dialog |
| SortHeader | Sortable column header with useSort hook |
| StatCard | Animated metric card with icon and variant colors |
| UserMenu | User avatar dropdown with role display and sign out |
| VoiceItemInput | Web Speech API voice-to-item matching with NLP-lite parsing |
| Hook | Purpose |
|---|---|
useIsMobile() |
Returns true when viewport < 768px |
usePersistedColumns(key, allKeys, defaults) |
Persist table column visibility to localStorage |
useRowSelection(rows) |
Generic row selection state for bulk operations |
useShowInactiveItems() |
Toggle inactive items visibility (localStorage persisted) |
useToast() |
Toast notification system (max 1 visible, auto-dismiss) |
| Module | Purpose | Key Exports |
|---|---|---|
| activityLog.ts | Audit logging | logActivity() — inserts to activity_log |
| gstExport.ts | GST return Excel files | exportGSTR1(), exportGSTR2(), exportGSTR3B() — B2B, B2CL, B2CS, CDNR, HSN sheets |
| importExport.ts | Bulk data import/export | Party/item/invoice/payment Excel/CSV import & export with templates |
| invoiceCalc.ts | Invoice math engine | calcLineAmount(), solveRateFromAmount(), computeInvoiceTotals() — supports tax-inclusive/exclusive, extra charges, GST split |
| invoicePdf.ts | Invoice PDF generation | Thermal 58mm/80mm, A5 (single/2-page), A4 formats; UPI QR codes; amount in words (Lakh/Crore) |
| paymentPdf.ts | Payment receipt generation | Thermal/A5/A4 receipt and voucher formats |
| razorpay.ts | Payment SDK | loadRazorpay(), openRazorpay() — lazy-loads Razorpay script and opens checkout modal |
| reportArtifacts.ts | Shareable report artifacts | buildReportArtifact() — generates HTML + XLSX base64 for all 14 report types |
| reportExport.ts | Direct report export | 28 export functions (PDF + Excel for each report type) |
| reportGenerators.ts | Report data computation | Pure functions: getSaleReportData(), getProfitLossData(), getStockSummaryData(), etc. |
| Function | Endpoint | Auth | Purpose |
|---|---|---|---|
| create-user | POST | JWT (admin/owner) | Create or invite user into a company. Checks seat limit. Creates auth user if new, inserts company_members |
| lookup-email | POST | None (public) | Phone-to-email lookup for phone login. Normalizes +91 prefix, matches last 10 digits |
| manage-user | POST | JWT (admin) | Update (email, password, name) or delete a user. Cannot delete self |
| razorpay-create-order | POST | JWT | Create Razorpay order for monthly (₹199) or lifetime (₹9,999) plan. Applies coupon discount. Returns order_id + key_id |
| razorpay-verify-payment | POST | JWT | Verify HMAC-SHA256 signature. Issue license via extend_or_create_paid_license. Record coupon redemption |
| razorpay-webhook | POST | Razorpay signature | Async payment event handler. Handles payment.captured, payment.failed, refund.processed. Uses timingSafeEqual |
verify_jwt = false in config.toml because they extract and validate JWT manuallyrazorpay-webhook uses Razorpay webhook signature verification (HMAC-SHA256)lookup-email is intentionally public but only returns email (no other PII)If business_state === party_state:
CGST = total_gst × 0.5
SGST = total_gst × 0.5
IGST = 0
Else:
CGST = 0
SGST = 0
IGST = total_gst
0%, 5%, 12%, 18%, 28%
When priceWithTax = true, the rate is treated as inclusive of GST:
baseRate = rate / (1 + gstRate/100)
taxableAmount = qty × baseRate × (1 - discount%)
gstAmount = taxableAmount × gstRate / 100
| Stage | Behavior |
|——-|———-|
| pre | Applied before GST — modifies the taxable base |
| post | Applied after GST — modifies the grand total only |
| Report | Purpose | Sheets Generated |
|---|---|---|
| GSTR-1 | Sales return | B2B (registered dealers), B2CL (>₹2.5L inter-state unregistered), B2CS (small unregistered), CDNR (credit/debit notes), HSN (HSN-wise summary) |
| GSTR-2 | Purchase return | B2B, CDNR |
| GSTR-3B | Summary return | 3.1 Outward Supplies, 4. Input Tax Credit, 6.1 Tax Payment |
All 36 states and union territories mapped with proper GST state codes for Place of Supply.
| Type | Description | Duration |
|---|---|---|
trial |
Auto-created on signup | Configurable via app_settings.trial_days (default 7) |
paid |
Purchased via Razorpay | Monthly (30 days) or Lifetime (~100 years) |
complimentary |
Issued by super admin | Custom duration |
| Plan | Price | Duration | Seats |
|---|---|---|---|
| Monthly | ₹199/month | 30 days | 3 |
| Lifetime | ₹9,999 one-time | ~100 years | 3 |
/billing (LicenseGate)extend_or_create_paid_license()redeem_license() RPCpercent (e.g., 20% off) or flat (e.g., ₹50 off)monthly, lifetime, or bothmax_uses (NULL = unlimited)expires_at timestampvalidate_coupon() RPC → returns { valid, discount, final_amount }UNIQUE(coupon_id, user_id))super_admin (platform level — super_admins table)
└── owner (company level)
└── admin
└── staff
└── member
└── party (external, portal-only)
| Capability | Owner | Admin | Staff | Member | Party |
|---|---|---|---|---|---|
| View company data | ✓ | ✓ | ✓ | ✓ | Limited |
| Create/edit records | ✓ | ✓ | ✓ | ✓ | ✗ |
| Delete records | ✓ | ✓ | ✗ | ✗ | ✗ |
| Manage members | ✓ | ✓ | ✗ | ✗ | ✗ |
| Edit payments | ✓ | ✓ | ✓* | ✗ | ✗ |
| Delete company | ✓ | ✗ | ✗ | ✗ | ✗ |
| View all companies (super admin) | ✗ | ✗ | ✗ | ✗ | ✗ |
*Staff can edit payments only if can_edit_payments = true on their membership.
Each non-owner/admin member has granular page_permissions stored as JSON in company_members:
| Permission | Description |
|---|---|
view |
Can see the page and its data |
create |
Can create new records |
edit |
Can modify existing records |
delete |
Can remove records |
Pages: dashboard, parties, items, sales, purchases, payment_in, payment_out, sale_return, purchase_return, reports, settings
company_id columnis_company_member(company_id, auth.uid())party_access table# Run tests
bun run test
# Run tests in watch mode
bun run test:watch
Configuration: vitest.config.ts with @testing-library/jest-dom matchers. Test setup in src/test/setup.ts.
# Run Playwright tests
npx playwright test
Configuration: playwright.config.ts with custom fixture in playwright-fixture.ts.
| Script | Command | Description |
|---|---|---|
dev |
vite |
Start dev server on port 8080 |
build |
vite build |
Production build |
build:dev |
vite build --mode development |
Development build with source maps |
preview |
vite preview |
Preview production build locally |
lint |
eslint . |
Run ESLint |
test |
vitest run |
Run unit tests once |
test:watch |
vitest |
Run unit tests in watch mode |
This project is proprietary software. All rights reserved.