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, offline-capable invoicing, accounting, inventory and retail platform built for Indian small-to-medium businesses. It runs as a single-page React app on Supabase (Postgres + Auth + Edge Functions + Storage), is installable as a PWA, and is also shipped as a thin Capacitor (Android/iOS) and Electron (desktop) shell around the hosted site.
Core accounting
Selling channels & industry modules
Platform
.bkp export/import with FK-aware orderingsuper.html build for cross-tenant businesses, users, licences and AI provider keyssrc/lib/currency.ts + business_profiles.multi_currency_enabled) that adds a per-invoice currency and exchange rate with live FX rates; it is off by default and has no toggle on the Settings page — it is switched from the Backup & Restore screen’s data/settings section| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript 5, Vite 5 (two entries: index.html app, super.html console) |
| Styling | Tailwind CSS 3 + typography plugin, shadcn/ui (Radix UI primitives), next-themes |
| State | React Context (Auth, Company, License, App, Expense, Documents, Fields, Restaurant, Theme) + TanStack React Query |
| Routing | React Router DOM v6 |
| Forms | React Hook Form + Zod (@hookform/resolvers) |
| Animations / charts | Framer Motion, Recharts |
| Backend | Supabase — PostgreSQL, Auth (GoTrue), Edge Functions (Deno), Storage |
| Offline | IndexedDB outbox + snapshot cache (src/lib/offline/*), no external dependency |
| Payments | Razorpay Checkout (script loaded on demand) + Orders API + webhook |
| AI | Gemini / OpenAI / Anthropic / OpenRouter / any OpenAI-compatible endpoint, with the Lovable AI gateway as a final fallback |
| Resend (share links, password reset) | |
| PDF / print | HTML-to-print via iframe (src/utils/printHtml.ts), jspdf + html2canvas (src/lib/htmlToPdf.ts) |
| Barcodes / QR | jsbarcode, qrcode, @zxing/browser (camera & USB scanning) |
| Excel/CSV | xlsx (SheetJS), papaparse |
| Voice input | Web Speech API (SpeechRecognition) |
| Packaging | PWA (public/manifest.webmanifest), Capacitor 8 (Android/iOS shell), Electron (electron/main.cjs) |
| Hosting | Vercel (vercel.json SPA rewrites) |
| Testing | Vitest + Testing Library, Playwright |
| Linting | ESLint 9 with TypeScript + React Hooks plugins |
| Package Manager | Bun (bun.lockb) |
┌───────────────────────────────────────────────────────────────────────┐
│ Clients │
│ Browser SPA / installed PWA • Capacitor Android+iOS shell │
│ Electron desktop shell • super.html (Super Admin console) │
└───────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────────────────────────────────────────────────┐
│ React application (src/) │
│ │
│ AuthContext ──► CompanyContext ──► LicenseContext ──► LicenseGate │
│ │ │
│ ▼ │
│ AppContext (parties, items, invoices, payments, profile, loyalty, │
│ stock adjustments) │
│ ├── ExpenseContext ├── DocumentsContext (docs + recurring) │
│ ├── FieldsContext └── RestaurantContext │
│ │ │
│ Pages (48) ── Components (60+) ── Hooks (10) ── utils/ + lib/ │
│ │ │
│ ▼ │
│ offline.from() ─────────────► IndexedDB (kv snapshot + outbox) │
│ (Proxy over supabase.from) ▲ │
│ │ │ replay when reachable │
│ ▼ │ │
│ sync engine (src/lib/offline/sync.ts) │
└───────────────────────────────────────────────────────────────────────┘
│ HTTPS / WebSocket
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Supabase │
│ ┌────────────────┐ ┌───────────────┐ ┌────────────────────────────┐ │
│ │ PostgreSQL │ │ Auth (GoTrue) │ │ Edge Functions (Deno) │ │
│ │ 50+ tables │ │ email + phone │ │ api (REST v1) │ │
│ │ RLS on all │ │ JWT sessions │ │ create-user, manage-user │ │
│ │ company- │ │ │ │ lookup-email │ │
│ │ scoped tables │ │ │ │ parse-invoice-file (AI) │ │
│ │ RPC helpers │ │ │ │ public-doc, public-report │ │
│ │ (has_role, │ │ │ │ send-doc-link, │ │
│ │ is_company_*, │ │ │ │ send-report-link, │ │
│ │ redeem_ │ │ │ │ send-password-reset │ │
│ │ license, sa_*)│ │ │ │ razorpay-* (3) │ │
│ └────────────────┘ └───────────────┘ │ loyalty-card-lookup │ │
│ ┌────────────────┐ │ super-admin │ │
│ │ Storage │ └────────────────────────────┘ │
│ │ shared-pdfs, │ │
│ │ item/store img │ │
│ └────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
Razorpay (orders, Resend (share & AI providers (Gemini,
checkout, webhook) reset emails) OpenAI, Anthropic,
OpenRouter, Lovable AI)
smartbooks/
├── public/
│ ├── manifest.webmanifest # PWA manifest (installable app)
│ ├── favicon.svg / favicon.ico / apple-touch-icon.png / pwa-192.png / pwa-512.png
│ ├── icons.svg # sprite used by print templates
│ └── robots.txt
├── index.html # main SPA entry
├── super.html # Super Admin console entry (separate bundle)
├── electron/main.cjs # Electron desktop shell (loads hosted site)
├── capacitor.config.ts # Android / iOS shell config
├── vercel.json # SPA rewrites for hosting
├── docs/
│ ├── FEATURES.md # plain-English feature list
│ ├── API.md # REST API v1 reference
│ └── smartbooks-api.postman_collection.json
├── src/
│ ├── main.tsx # app entry — starts the offline sync engine, mounts App
│ ├── App.tsx # providers, LicenseGate, all route definitions
│ ├── index.css / App.css # Tailwind layers + design tokens
│ ├── assets/ # hero and auth imagery
│ ├── components/
│ │ ├── AppLayout.tsx # shell: sidebar + header + content + HeaderPortal
│ │ ├── AppSidebar.tsx # permission-filtered navigation, module toggles
│ │ ├── NetworkStatus.tsx # online/offline dot, pending count, manual sync
│ │ ├── AlertsBell.tsx # low stock, overdue parties, licence expiry
│ │ ├── CompanySwitcher.tsx # switch/create companies
│ │ ├── UserMenu.tsx / ThemeToggle.tsx / NavLink.tsx / HeaderPortal.tsx
│ │ ├── InvoiceUploadDialog.tsx # AI bill upload → prefilled invoice panel
│ │ ├── DocumentItemsTable.tsx # shared spreadsheet line grid (invoices + documents)
│ │ ├── InlineItemSearch.tsx / LineItemNameInput.tsx / ItemPickerDialog.tsx
│ │ ├── QuickAddItemDialog.tsx / QuickAddPartyDialog.tsx / PartyCombobox.tsx
│ │ ├── ExtraChargesEditor.tsx / SignaturePad.tsx / SpreadsheetEditor.tsx
│ │ ├── InvoicePreviewDialog.tsx / InvoiceFullPreview.tsx / PaymentPreviewDialog.tsx
│ │ ├── InvoiceDesigner.tsx / InvoiceCanvasDesigner.tsx / BackgroundEditor.tsx
│ │ ├── CustomFieldsManager.tsx / CustomFieldsSection.tsx / IndustrySettingsPanel.tsx
│ │ ├── EwayBillSection.tsx / EwaySettingsPanel.tsx
│ │ ├── PosSettingsPanel.tsx / BillingZoomControl.tsx / BarcodeScannerDialog.tsx
│ │ ├── BulkBarcodePrintDialog.tsx / BulkActionBar.tsx / SortHeader.tsx / StatCard.tsx
│ │ ├── KotOrderDialog.tsx / RestaurantSettingsPanel.tsx
│ │ ├── LoyaltyCardPrintDialog.tsx / LoyaltyRedeemCard.tsx / LoyaltyRedeemPanel.tsx
│ │ │ / PartyLoyaltyFields.tsx / RewardsPosSettings.tsx
│ │ ├── ShareLinkPanel.tsx / SendDocLinkDialog.tsx / ShareReportDialog.tsx
│ │ │ / ReportViewDialog.tsx / SentReportsTab.tsx / SharedReportsTab.tsx
│ │ ├── WhatsAppShareDialog.tsx / WhatsAppSettingsPanel.tsx
│ │ ├── CouponsAdmin.tsx / LicenseHistory.tsx / SavingOverlay.tsx / SmartBooksLogo.tsx
│ │ └── ui/ # shadcn/ui library (48 primitives)
│ ├── contexts/
│ │ ├── AuthContext.tsx # Supabase session, user, profile
│ │ ├── CompanyContext.tsx # memberships, active company, usePermissions()
│ │ ├── LicenseContext.tsx # licence state, isExpired, isSuperAdmin
│ │ ├── AppContext.tsx # core data store (offline-aware CRUD)
│ │ ├── ExpenseContext.tsx # expenses + expense payments
│ │ ├── DocumentsContext.tsx # estimates/orders/challans + recurring invoices
│ │ ├── FieldsContext.tsx # custom fields + active industry pack
│ │ ├── RestaurantContext.tsx # areas, tables, KOTs, reservations
│ │ └── ThemeContext.tsx # light/dark/system
│ ├── hooks/ # use-mobile, use-toast, use-sync-status,
│ │ # use-billing-zoom, use-custom-templates,
│ │ # use-hidden-columns, use-persisted-columns,
│ │ # use-row-activate, use-row-selection,
│ │ # use-show-inactive-items
│ ├── industries/ # pharma, manufacturing, garment, jewellery,
│ │ # restaurant packs + shared types
│ ├── integrations/supabase/ # client.ts singleton, generated types.ts
│ ├── lib/
│ │ ├── offline/idb.ts # IndexedDB kv + outbox stores
│ │ ├── offline/sync.ts # outbox queue, reachability probe, replay loop
│ │ ├── offline/offlineClient.ts # offline.from() Proxy over supabase.from()
│ │ ├── currency.ts # optional multi-currency + live FX rates
│ │ ├── i18n.ts # 11-language invoice dictionary
│ │ ├── barcode.ts / loyalty.ts / loyaltyCard.ts
│ │ ├── shareLinks.ts / reportShare.ts
│ │ ├── whatsapp.ts / whatsappDocs.ts
│ │ ├── htmlToPdf.ts / backgroundSamples.ts / utils.ts
│ ├── pages/ # see §10.3 for the full list (48 pages)
│ ├── super/ # Super Admin console (own entry point)
│ │ ├── main.tsx / SuperConsole.tsx / lib/api.ts
│ │ └── pages/SuperOverview|SuperBusinesses|SuperUsers|SuperLicenses|SuperAiProviders
│ ├── test/ # setup.ts, example.test.ts, api-contract.test.ts
│ ├── types/ # index.ts, documents.ts, expense.ts, restaurant.ts
│ └── utils/ # calculation, print, export and report modules (§10.6)
├── supabase/
│ ├── config.toml # edge function JWT settings
│ ├── functions/ # 15 edge functions (§11)
│ └── manual-migrations/ # 001 → 042 SQL scripts, run in order (§9)
├── package.json / bun.lockb
├── vite.config.ts # port 8080, two build entries
├── tailwind.config.ts / postcss.config.js / components.json
├── tsconfig*.json / eslint.config.js
└── playwright.config.ts / playwright-fixture.ts
# 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 secrets (set in the Supabase dashboard → Edge Functions → Secrets):
| Secret | Used by | Required for |
|---|---|---|
SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY |
all functions | injected by Supabase; anon key is used to build RLS-scoped clients |
RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRET |
razorpay-create-order, razorpay-verify-payment |
online licence purchase |
RAZORPAY_WEBHOOK_SECRET |
razorpay-webhook |
async payment/refund events |
RESEND_API_KEY |
send-doc-link, send-report-link, send-password-reset |
all outbound email |
RESET_FROM_EMAIL |
send-password-reset (+ fallback sender elsewhere) |
password reset mails |
SHARE_FROM_EMAIL |
send-doc-link, send-report-link |
optional sender override for share mails |
GEMINI_API_KEY |
parse-invoice-file |
AI invoice reading (fallback after ai_providers rows) |
LOVABLE_API_KEY |
parse-invoice-file |
last-resort AI gateway |
Additional AI provider keys (OpenAI, Anthropic, OpenRouter, any OpenAI-compatible endpoint)
are not env vars — they live in the service-role-only ai_providers table and are
managed from Super Admin → AI providers.
┌─────────────┐ ┌──────────────┐ ┌────────────────┐
│ 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 (two tabs) │ │
│ │ Email & link: │ │
│ │ ├─ buildReportArtifact(HTML + XLSX64) │ │
│ │ ├─ createReportShare() → shared_reports │ │
│ │ │ row with public_token │ │
│ │ ├─ [Create view link] → /r/:token, copied│ │
│ │ └─ [Send email] → send-report-link fn │ │
│ │ (Resend, logs report_share_emails) │ │
│ │ Portal users: │ │
│ │ ├─ Select party-portal recipients │ │
│ │ └─ Insert into shared_reports table │ │
│ │ │ │
│ │ Public link: /r/:token → PublicReport page → │ │
│ │ public-report edge fn (no sign-in) → view, │ │
│ │ print/PDF or Excel; view_count incremented │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌─── 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, expenses, expense_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: expense_payments → payments → │ │
│ │ invoice_items → invoices → party_access │ │
│ │ → expenses → items → parties → │ │
│ │ activity_log → shared_reports → │ │
│ │ custom_units → categories → │ │
│ │ business_profiles │ │
│ │ 3. Upsert in batches of 500: │ │
│ │ Reverse order (business_profiles first, │ │
│ │ parties, items, expenses, 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) │ │
│ └──────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
SmartBooks keeps working when the network drops. Every write is described declaratively, queued in IndexedDB and replayed in order once connectivity returns. Reads fall back to the last cached snapshot, so the app still renders invoices, parties, items and settings with no connection at all.
Modules
| File | Responsibility |
|---|---|
src/lib/offline/idb.ts |
Minimal IndexedDB helper. Two stores: kv (cached server snapshots) and outbox (queued writes). |
src/lib/offline/sync.ts |
Sync engine: connectivity state, offlineWrite(), durable outbox, syncNow(), auto-sync timer and reachability probe. |
src/lib/offline/offlineClient.ts |
offline.from(table) — a drop-in proxy over supabase.from() that queues insert/update/delete when offline. |
src/hooks/use-sync-status.ts |
useSyncStatus() — live { online, pending, syncing, autoSync, lastSyncedAt, lastError }. |
src/components/NetworkStatus.tsx |
Header indicator: green dot online, grey dot offline, pending badge, auto-sync toggle, Sync now. |
src/contexts/AppContext.tsx |
Hydrates from the IndexedDB snapshot, persists after every change, refreshes when the outbox flushes. |
Write path (online vs offline)
┌──────────────────────────┐
│ User saves invoice / │
│ payment / party / item │
└────────────┬─────────────┘
│
▼
┌────────────────┐
│ offlineWrite() │
└───┬────────┬───┘
online │ │ offline OR network error
▼ ▼
┌──────────────────┐ ┌───────────────────────────┐
│ Supabase / RLS │ │ IndexedDB `outbox` │
│ real insert │ │ { id, table, op, payload, │
│ │ │ match, createdAt } │
└────────┬─────────┘ └─────────────┬─────────────┘
│ │
│ success │ optimistic success
▼ ▼
┌──────────────────────────────────────────────────┐
│ UI updates immediately + snapshot saved to `kv` │
└──────────────────────────────────────────────────┘
Sync loop
┌─────────┐ browser 'online' event ┌──────────┐
│ OFFLINE │ ──────────────────────▶ │ ONLINE │
│ grey dot│ ◀────────────────────── │ green dot│
└─────────┘ 'offline' / probe fail └────┬─────┘
▲ │
│ every 15s probe│(SELECT 1 on companies)
│ ▼
│ ┌────────────────────┐
│ │ autoSync && pending│
│ └─────────┬──────────┘
│ │ yes
│ ▼
│ ┌────────────────────┐
│ │ syncNow() │
│ │ replay outbox in │
│ │ createdAt order │
│ └───┬────────┬───────┘
│ network error │ │ permanent error (RLS/validation)
└────────────────────────────┘ ▼
┌──────────────────────────┐
│ drop entry + surface │
│ lastError (queue can't │
│ wedge) │
└───────────┬──────────────┘
│ all done
▼
┌──────────────────────────┐
│ lastSyncedAt stamped │
│ onOutboxFlushed() → │
│ AppContext full refresh │
└──────────────────────────┘
Behaviour notes
localStorage as sb_auto_sync).navigator.onLine is trusted only as a hint — a lightweight Supabase query
confirms real reachability every 15 seconds (captive-portal safe).crypto.randomUUID() primary key, so
replayed rows keep the same id the UI already showed.┌─────────────────────────────────────────────────────────────────────┐
│ 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)│
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌────────────────┐
│ expenses │ │expense_payments│
│(per company) │ │(per company) │
└──────┬───────┘ └────────────────┘
│
▼
┌──────────────┐
│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 |
invoice_font_family |
text |
NOT NULL, DEFAULT '' |
invoice_language |
text |
NOT NULL, DEFAULT 'en' |
multi_currency_enabled |
boolean |
NOT NULL, DEFAULT false |
base_currency |
text |
NOT NULL, DEFAULT 'INR' |
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_reportsReport snapshots shared with portal users and/or via a public view link
(migration 042_report_share_links.sql).
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
shared_by |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
recipient_user_id |
uuid |
FK → auth.users(id) CASCADE (null for email-only shares) |
recipient_email |
text |
Email recipient of the view link |
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 |
public_token |
uuid |
NOT NULL, UNIQUE, DEFAULT gen_random_uuid() — slug of /r/:token |
public_enabled |
boolean |
NOT NULL, DEFAULT true (revoke the link) |
expires_at |
timestamptz |
Optional link expiry |
view_count |
integer |
NOT NULL, DEFAULT 0 |
last_viewed_at |
timestamptz |
|
last_sent_at |
timestamptz |
|
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
viewed_at |
timestamptz |
|
downloaded_at |
timestamptz |
report_share_emailsDelivery log for report view links emailed through the send-report-link function.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK |
report_id |
uuid |
FK → shared_reports(id) CASCADE |
user_id |
uuid |
FK → auth.users(id) SET NULL |
to_email |
text |
NOT NULL |
message |
text |
|
status |
text |
NOT NULL, DEFAULT 'sent' (sent | failed) |
error |
text |
|
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
expensesBusiness expenses with GST, vendor linking, payment tracking, and recurrence.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK, gen_random_uuid() |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
user_id |
uuid |
NOT NULL, FK → auth.users(id) CASCADE |
date |
date |
NOT NULL, DEFAULT CURRENT_DATE |
category |
text |
NOT NULL, DEFAULT 'Miscellaneous' |
vendor_id |
uuid |
FK → parties(id) SET NULL |
vendor_name |
text |
|
description |
text |
NOT NULL, DEFAULT '' |
amount |
numeric(14,2) |
NOT NULL, DEFAULT 0 |
gst_rate |
numeric(5,2) |
NOT NULL, DEFAULT 0 |
gst_amount |
numeric(14,2) |
NOT NULL, DEFAULT 0 |
is_inter_state |
boolean |
NOT NULL, DEFAULT false |
total |
numeric(14,2) |
NOT NULL, DEFAULT 0 |
itc_eligible |
boolean |
NOT NULL, DEFAULT true |
payment_status |
text |
NOT NULL, DEFAULT 'unpaid' (paid|unpaid|partial) |
attachment_data_url |
text |
|
notes |
text |
|
recurrence |
text |
NOT NULL, DEFAULT 'none' (none|daily|weekly|monthly|quarterly|yearly) |
next_due_date |
date |
|
recurring_parent_id |
uuid |
FK → expenses(id) SET NULL |
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
updated_at |
timestamptz |
NOT NULL, DEFAULT now() |
expense_paymentsPayments made against an expense.
| Column | Type | Constraints |
|---|---|---|
id |
uuid |
PK, gen_random_uuid() |
expense_id |
uuid |
NOT NULL, FK → expenses(id) CASCADE |
company_id |
uuid |
NOT NULL, FK → companies(id) CASCADE |
date |
date |
NOT NULL, DEFAULT CURRENT_DATE |
amount |
numeric(14,2) |
NOT NULL, DEFAULT 0 |
mode |
text |
NOT NULL, DEFAULT 'cash' (cash|bank|upi|cheque|card) |
notes |
text |
|
created_at |
timestamptz |
NOT NULL, DEFAULT now() |
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) |
These tables are all company-scoped (company_id + RLS via is_company_member /
can_write_company) unless noted.
| Table | Migration | Purpose | Notable columns |
|---|---|---|---|
invoice_templates |
024 | Saved custom invoice-designer layouts | template_id, design jsonb |
loyalty_transactions |
028 | Signed points ledger | party_id, invoice_id, points, kind (earn/redeem/adjust/bonus/expire) |
stock_adjustments |
029 | Manual stock corrections | item_id, qty (signed), reason, note |
documents |
036 | Estimates, sale orders, delivery challans, purchase orders | type (document_type), items jsonb, converted_invoice_id |
recurring_invoices |
036 | Recurring invoice schedules | frequency, next_run_date, history jsonb |
restaurant_areas |
037 | Seating zones | name, sort_order |
restaurant_tables |
037 | Tables and live status | area_id, capacity, status, current_invoice_id |
kots |
037 | Kitchen order tickets | table_id, items jsonb, status, order_type |
reservations |
037 | Table bookings | guest_name, phone, reserved_at, covers, status |
store_settings |
026, 027, 030 | Storefront config per company | unique slug, enabled, logo/banner/theme, allow_orders, min_order_value |
store_items |
026, 027 | Catalogue visibility & overrides | item_id, store_price, image_url, featured, sort_order |
store_customers |
026 | Public shopper profiles (1:1 with auth.users) |
created by signup trigger |
store_orders |
026 | Storefront orders | status (store_order_status), invoice_id, totals |
store_order_items |
026 | Order lines | order_id, item_id, qty, rate |
webhook_endpoints |
038 | Outbound webhook registrations (REST API) | url, secret, events |
webhook_deliveries |
038 | Delivery attempt log | endpoint_id, event, status_code, error |
doc_shares |
039, 040 | Public invoice/payment link control (row UUID is the slug) | kind, doc_id, revoked, expires_at, password_hash, view_count, last_viewed_at, last_sent_at |
doc_share_emails |
040 | Invoice/payment share email log | to_email, sent_at, status |
report_share_emails |
042 | Report share email log | report_id, to_email, sent_at |
ai_providers |
041 | AI provider credentials for the invoice reader | provider, model, api_key, base_url, priority, last_used_at, last_error — no anon/authenticated grants; reachable only via service role and the sa_ai_provider_* RPCs |
| Enum | Values | Usage |
|---|---|---|
app_role |
admin, staff, user, party |
Global (legacy) user roles, still read by has_role() |
company_role |
owner, admin, staff, member, party |
Per-company roles — the live RBAC model |
license_type |
trial, paid, complimentary |
License classification |
license_status |
active, expired, revoked |
License state |
plan_code |
monthly, lifetime, year1, year3 |
Subscription plan (year1/year3 added in 031) |
coupon_type |
percent, flat |
Discount calculation mode |
coupon_applies_to |
monthly, lifetime, both, year1, year3 |
Plan-specific coupons |
payment_status |
created, paid, failed, refunded |
Razorpay payment state |
store_order_status |
pending, accepted, rejected, fulfilled, cancelled |
Online store orders |
document_type |
estimate, sale_order, delivery_challan, purchase_order |
Non-invoice documents |
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. Executable by anon |
record_coupon_use |
(uuid) → void |
Idempotent coupon usage counter (service role only) — migration 033 |
company_license_active |
(uuid) → boolean |
Gates the public storefront on a live licence — migration 026 |
store_public_catalog |
(text slug) → jsonb |
Anon RPC returning storefront branding + catalogue — 026, replaced in 030 |
store_place_order |
(...) → jsonb |
Anon/authenticated RPC: validates the cart, prices server-side, writes store_orders |
store_my_orders |
() → jsonb |
Current shopper’s orders with invoice/payment status |
store_slug_available |
(text, uuid) → boolean |
Storefront slug uniqueness check |
handle_new_store_customer |
() → trigger |
Creates a store_customers row when signup metadata marks a shopper |
lookup_loyalty_card |
(phone, card_number) → jsonb |
Anon RPC behind the public /cards portal — 032, replaced in 034 (adds card design) |
sa_business_overview / sa_business_breakdown / sa_usage_by_table / sa_storage_usage / sa_db_stats |
(…) → jsonb |
Super Admin console metrics — migration 035 |
sa_users / sa_grant_access / sa_revoke_access / sa_delete_business |
(…) |
Super Admin cross-tenant user and business management (cascading delete across every company_id table + storage objects) |
sa_ai_providers / sa_ai_provider_save / sa_ai_provider_delete |
(…) |
Masked-key CRUD over ai_providers, super admin only — migration 041 |
sa_assert_super_admin |
() → void |
Guard used by every sa_* function |
| 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_expenses_updated_at |
expenses |
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() |
on_auth_user_created_store_customer |
auth.users |
AFTER INSERT | handle_new_store_customer() |
| 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 |
expenses_company_id_idx |
expenses | company_id |
expenses_date_idx |
expenses | date |
expenses_vendor_id_idx |
expenses | vendor_id |
expense_payments_expense_id_idx |
expense_payments | expense_id |
expense_payments_company_id_idx |
expense_payments | company_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 |
| 023 | — | Expenses + profile settings | expenses + expense_payments tables; invoice_font_family, invoice_language, multi_currency_enabled, base_currency on business_profiles |
| 024 | — | Invoice templates | invoice_templates (saved designer layouts, design jsonb) + RLS |
| 025 | — | Custom fields + industry packs | custom_fields jsonb on items/parties/invoices/invoice_items; industry_settings, custom_field_defs, signature_url on business_profiles; GIN indexes |
| 026 | — | Online store | store_settings, store_items, store_customers (+ signup trigger), store_orders, store_order_items, store_order_status enum, company_license_active(), store_public_catalog/place_order/my_orders/slug_available() |
| 027 | — | Store branding + alerts | Storefront branding/customisation columns; business tagline/website/brand colours; items.low_stock_alert; parties.payment_alert_enabled/payment_due_days; explicit Data-API grants |
| 028 | — | POS, e-way, loyalty | pos_settings/eway_settings/loyalty_settings on business_profiles; loyalty + e-way + pos_sale/payment_mode columns on parties/invoices; loyalty_transactions table |
| 029 | — | Stock adjustments | stock_adjustments table (signed qty, reason, note) + RLS |
| 030 | — | Storefront catalogue rewrite | Replaces store_public_catalog() to expose 027 branding fields and hide out-of-stock items |
| 031 | — | 1-year / 3-year plans | Adds year1, year3 to plan_code and coupon_applies_to |
| 032 | — | Loyalty card portal | lookup_loyalty_card() anon RPC for the public /cards portal |
| 033 | — | Coupon usage tracking | record_coupon_use() (idempotent) + backfill of historical redemptions |
| 034 | — | Card portal design | Replaces lookup_loyalty_card() to return card design + member phone |
| 035 | — | Super Admin console | sa_* RPCs: usage/storage/DB stats, business overview & breakdown, user listing, access grant/revoke, cascading sa_delete_business() |
| 036 | — | Documents + recurring | document_type enum, documents (estimate/sale order/delivery challan/purchase order), recurring_invoices |
| 037 | — | Restaurant module | restaurant_settings on business_profiles; restaurant_areas, restaurant_tables, kots, reservations with generated RLS |
| 038 | — | API webhooks | webhook_endpoints, webhook_deliveries for the REST API’s outbound events |
| 039 | — | Doc share links | doc_shares — revoke/expiry control for public invoice & payment links |
| 040 | — | Share centre | doc_shares password hash, view counts and timestamps; doc_share_emails log |
| 041 | — | AI providers | ai_providers (service-role only) + masked-key sa_ai_provider_* RPCs |
| 042 | — | Report share links | shared_reports gains public_token, public_enabled, recipient_email, view stats, expires_at; recipient_user_id nullable; report_share_emails log |
Run order matters. The scripts are idempotent where practical, but 013 must complete before 014, and 040/042 assume 039/012 have run. See
supabase/manual-migrations/README.mdfor the multi-tenancy run book.
<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)
<ExpenseProvider> ← Expense CRUD (DB-backed, per-company)
<AppLayout> ← Sidebar, header, content area
{page}
</AppLayout>
</ExpenseProvider>
</AppProvider>
</LicenseProvider>
</CompanyProvider>
</AuthProvider>
</BrowserRouter>
</TooltipProvider>
</QueryClientProvider>
All routing lives in AppRoutes() in src/App.tsx, which branches by user type before mounting <Routes>.
Always public (no session needed, mounted outside the auth branch):
| Path | Page | Purpose |
|——|——|———|
| /d/i/:id | PublicDoc | Shared invoice view (expiry, revocation, optional password) |
| /d/p/:id | PublicDoc | Shared payment receipt view |
| /r/:token | PublicReport | Shared report snapshot (print/PDF + Excel) |
Signed out:
| Path | Page |
|——|——|
| / | Landing |
| /auth | Auth (single email-or-phone field, signup toggle) |
| /reset-password | ResetPassword |
| /store/:slug | StoreFront (public storefront) |
| /cards | CardsPortal (public loyalty card lookup) |
| * | → /auth?redirect=… |
Store customers (signed in, no company membership): /store/:slug, /cards, /my-orders (MyOnlineOrders), /reset-password, * → /my-orders.
Party portal users (company_role = party, no licence check): /portal (PartyPortal), /store/:slug, /cards, /my-orders, * → /portal.
Full app (signed in, licence-gated by LicenseGate):
| Path | Page | Permission key |
|——|——|—————-|
| / | Dashboard | dashboard |
| /parties, /party-ledger/:id | Parties, PartyLedger | parties |
| /items, /item-history/:id | Items, ItemHistory | items |
| /sales, /sales/new, /sales/edit/:id | Sales, CreateInvoice, EditInvoice | sales |
| /purchases | Purchases | purchases |
| /pos | Pos | sales |
| /loyalty | Loyalty | parties |
| /payment-in, /payment-out | PaymentIn, PaymentOut | payment_in, payment_out |
| /sale-return, /purchase-return | SaleReturn, PurchaseReturn | sale_return, purchase_return |
| /estimates, /sale-orders, /delivery-challans | DocumentsPage | sales |
| /purchase-orders | DocumentsPage | purchases |
| /documents/:docType/new, /documents/:docType/edit/:id | DocumentEditor | sales |
| /recurring-invoices, /recurring-invoices/new, /recurring-invoices/edit/:id | RecurringInvoices, RecurringEditor | sales |
| /tables, /kot, /reservations | RestaurantTables, KitchenDisplay, Reservations | sales |
| /expenses | Expenses | payment_out |
| /reports | Reports | reports |
| /shared-links | SharedLinks | sales |
| /settings, /settings/eway, /settings/pos | SettingsPage, EwaySettings, PosSettings | settings |
| /backup | BackupRestore | settings |
| /store | OnlineStore | — |
| /admin | AdminPanel | self-gated by role |
| /billing | Billing | exempt from LicenseGate |
| /portal | PartyPortal | any signed-in user with party access |
| * | NotFound | — |
LicenseGate: when a company’s licence is missing or expired, every route except /billing redirects to the billing page.
Super Admin console: not part of this router. super.html is a second Vite entry that mounts src/super/SuperConsole.tsx, which checks membership of the super_admins table before rendering Overview, Businesses, Users, Licences and AI providers.
| 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 | 15 report types | PDF/Excel export, share to portal, GSTR-1/2/3B, Expense Report |
| Expenses | Expense tracking | Category-wise expenses, GST/ITC, vendor linking, payment tracking, recurrence |
| 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, invoice language, invoice font, multi-currency |
| AdminPanel | Team management | Member CRUD, roles, page permissions, activity log, app settings |
| Billing | License management | Plans, Razorpay payment, coupon apply, key redemption |
Super console (src/super/*) |
Cross-tenant admin (separate super.html build) |
Businesses, users, licences/keys, coupons, DB & storage usage, AI provider keys |
| Pos / PosSettings | Touch billing | Item tiles, keypad, barcode scan, tender modes, thermal receipts |
| DocumentsPage / DocumentEditor | Estimates, sale & purchase orders, delivery challans | Same line grid as invoices, convert to invoice |
| RecurringInvoices / RecurringEditor | Recurring schedules | Frequency, next run, auto-generation history |
| RestaurantTables / KitchenDisplay / Reservations | Restaurant floor | Table status, KOTs, bookings, table-to-bill |
| Loyalty / CardsPortal | Rewards | Points rules, tiers, redemption, printable cards, public lookup |
| OnlineStore / StoreFront / MyOnlineOrders | Online selling | Catalogue, storefront branding, orders, order-to-invoice |
| SharedLinks | Share centre | All public invoice/payment links with status, views, expiry, password, email |
| PublicDoc / PublicReport | Sign-in-free views | Shared invoice, receipt and report pages |
| EwaySettings | E-way bill defaults | Transporter defaults and auto-open threshold |
| ResetPassword | Password reset | Resend-delivered reset flow |
| 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 |
| LineItemNameInput / InlineItemSearch | In-grid item search with voice (Web Speech API) and fuzzy matching |
| NetworkStatus | Online/offline dot, pending-change count, auto-sync toggle, manual sync |
| AlertsBell | Low stock, overdue party payments, licence expiry alerts |
| InvoiceUploadDialog | AI bill upload (PDF/JPG/PNG) with progress, error detail and prefill hand-off |
| DocumentItemsTable | Shared spreadsheet line grid used by invoices and documents |
| ShareLinkPanel / SendDocLinkDialog | Public link creation, password, expiry, revoke and email delivery |
| ReportViewDialog / ShareReportDialog | In-app report view; email + public link + portal sharing |
| InvoiceDesigner / InvoiceCanvasDesigner / BackgroundEditor | Custom invoice layout designer |
| CustomFieldsManager / IndustrySettingsPanel | Custom field defs and industry packs |
| BarcodeScannerDialog / BulkBarcodePrintDialog | Camera/USB scanning and label sheet printing |
| KotOrderDialog / RestaurantSettingsPanel | Restaurant order tickets and module settings |
| LoyaltyRedeemPanel / LoyaltyCardPrintDialog | Points redemption and card printing |
| WhatsAppShareDialog / WhatsAppSettingsPanel | WhatsApp message templates and sharing |
| LicenseHistory / SavingOverlay / ThemeToggle / HeaderPortal | Licence history, save feedback, theming, header slots |
| Hook | Purpose |
|---|---|
useIsMobile() |
Returns true when viewport < 768px |
useToast() |
Toast notification system (max 1 visible, auto-dismiss) |
useSyncStatus() |
Subscribes to the offline sync engine (online, pending, syncing, autoSync, lastSyncedAt) |
usePersistedColumns(key, allKeys, defaults) |
Persist table column visibility to localStorage |
useHiddenColumns(...) |
Hide/show columns on the invoice & document line grids |
useRowSelection(rows) |
Row selection state for bulk operations |
useRowActivate(...) |
Keyboard/click row activation for list pages |
useShowInactiveItems() |
Toggle inactive items visibility (localStorage persisted) |
useBillingZoom() |
Zoom level for the billing/POS panel |
useCustomTemplates() |
Load/save custom invoice designer templates (invoice_templates) |
| Module | Purpose | Key Exports |
|---|---|---|
| activityLog.ts | Audit logging | logActivity() — inserts to activity_log |
| invoiceCalc.ts | Invoice math engine | calcLineAmount(), solveRateFromAmount(), computeInvoiceTotals() — tax-inclusive/exclusive, extra charges, GST split |
| invoicePdf.ts | Invoice print/PDF | Thermal 58/80 mm, A5 (single/2-page), A4; UPI QR; amount in words (Lakh/Crore) |
| invoiceGstTemplates.ts | Statutory GST invoice layouts | A4/A5 GST templates with HSN summary, language + font support |
| customInvoiceTemplate.ts | Designer output | Renders saved invoice_templates designs to print HTML |
| invoiceFieldPrint.ts | Custom fields on print | Maps custom field defs to invoice columns/blocks |
| printHtml.ts | Print pipeline | Opens generated HTML in a hidden iframe for print/PDF |
| documentPrint.ts | Documents print | Estimate / order / challan print layouts |
| kotPrint.ts | Kitchen tickets | Thermal KOT layout for the restaurant module |
| paymentPdf.ts | Payment receipts | Thermal/A5/A4 receipt and voucher formats |
| statementPdf.ts | Party statements | Ledger/statement PDF with running balance |
| gstExport.ts | GST return Excel | exportGSTR1/2/3B() — B2B, B2CL, B2CS, CDNR, HSN sheets |
| gstReturns.ts | GST return HTML | gstr1Html, gstr2Html, gstr3bHtml, gstr9Html shared by view + export |
| reportGenerators.ts | Report data computation | Pure data functions for all 18 reports + expenses |
| reportArtifacts.ts | Shareable artifacts | buildReportArtifact() — HTML + XLSX base64 used by view, export and share |
| reportExport.ts | Direct export | PDF + Excel export per report type |
| expenseReports.ts | Expense reporting | Category breakdown + ITC summary export |
| importExport.ts | Bulk import/export | Party/item/invoice/payment/expense Excel & CSV with templates and validation |
| bulkExport.ts | Multi-record export | Bulk PDF/Excel export from list pages |
| razorpay.ts | Payment SDK | loadRazorpay(), openRazorpay() — lazy-loads checkout |
| 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 |
| send-password-reset | POST | None (public) | Sends a password-reset link for an email/phone account |
| loyalty-card-lookup | POST | None (public) | Customer-facing card portal lookup by phone + card number |
| public-doc | GET/POST | None (public) | Serves a shared invoice/payment by document id; enforces doc_shares revocation, expiry and SHA-256 password gate; tracks view counts |
| public-report | GET/POST | None (public) | Serves a shared report snapshot by public_token; enforces public_enabled/expires_at; tracks view counts |
| send-doc-link | POST | JWT (RLS-verified) | Emails an invoice/payment share link via Resend; logs to doc_share_emails |
| send-report-link | POST | JWT (RLS-verified) | Emails a report share link via Resend; logs to report_share_emails |
| parse-invoice-file | POST | JWT | AI invoice/bill OCR. Accepts base64 PDF or image, returns structured invoice JSON (party, dates, line items, discounts, GST, totals). Tries providers from ai_providers by priority, then GEMINI_API_KEY, then the Lovable AI gateway. The file is never persisted |
| super-admin | POST | JWT (super admin) | Cross-tenant console actions: businesses, users, licences, storage/DB stats |
| api | GET/POST/PATCH/DELETE | JWT (manual) | Public REST API v1 over every business resource + report endpoints. See docs/API.md |
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 tableInvoiceUploadDialog (drag-and-drop or file picker), from Sales → Import and Purchases → Importparse-invoice-file edge function — never uploaded to Storage or stored in the DBsessionStorage (sb:invoice-prefill) and CreateInvoice opens prefilled at /sales/new?type=sale|purchase&prefill=1ai_providers rows (Gemini, OpenAI, Anthropic, OpenRouter, any OpenAI-compatible endpoint) → GEMINI_API_KEY secret → Lovable AI gateway (LOVABLE_API_KEY)last_used_at / last_error recorded in ai_providersSuperAiProviders, backed by migration 041_ai_providers.sql, service-role-only table + super-admin RPCs)src/lib/currency.ts: 17 currencies, live FX rates from open.er-api.com with a 1-hour cache, convert() / formatMoney()CreateInvoice shows a currency + exchange-rate selector and stores currencyCode/exchangeRate on the invoicebusiness_profiles.multi_currency_enabled / base_currency (mirrored to localStorage)gstr1Html / gstr2Html / gstr3bHtml / gstr9Html in src/utils/gstReturns.ts)
are used by both paths via standaloneHtml in src/utils/reportArtifacts.tsdocuments table)src/components/DocumentItemsTable.tsx) identical to
the invoice panel: inline item search, item picker, quick-add item, drag reorder,
HSN/qty/unit/MRP/rate/disc %/disc ₹/GST %/GST ₹/amount columns with reverse-calcpos_sale + payment_moderestaurant_areas, restaurant_tableskotsreservationsloyalty_transactions)/cards portal (edge function lookup)store_settings, store_items)store_orders, store_order_items, store_customers)stock_adjustments)super.html entry with businesses, users, licences and DB/storage usage viewssa_* RPCs and the super-admin edge function# 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 |
| Module | Pages | Contexts | Key tables |
|---|---|---|---|
| Core billing | Sales, Purchases, CreateInvoice, EditInvoice, SaleReturn, PurchaseReturn |
AppContext |
invoices, invoice_items |
| Parties & items | Parties, Items, PartyLedger, ItemHistory |
AppContext, FieldsContext |
parties, items, categories, custom_units, stock_adjustments |
| Money | PaymentIn, PaymentOut, Expenses |
AppContext, ExpenseContext |
payments, expenses, expense_payments |
| Documents | DocumentsPage, DocumentEditor, RecurringInvoices, RecurringEditor |
DocumentsContext |
documents, recurring_invoices |
| POS | Pos, PosSettings |
AppContext |
invoices (pos_sale) |
| Restaurant | RestaurantTables, KitchenDisplay, Reservations |
RestaurantContext |
restaurant_areas, restaurant_tables, kots, reservations |
| Loyalty | Loyalty, CardsPortal |
AppContext |
loyalty_transactions, parties.loyalty_config |
| Online store | OnlineStore, StoreFront, MyOnlineOrders |
— | store_settings, store_items, store_orders, store_order_items, store_customers |
| Reports | Reports |
AppContext, ExpenseContext |
reads all business tables, writes shared_reports |
| Admin & billing | AdminPanel, Billing, BackupRestore, SettingsPage, EwaySettings |
CompanyContext, LicenseContext |
company_members, licenses, coupons, subscription_payments, business_profiles, invoice_templates, activity_log |
| Super console | src/super/* (super.html) |
— | super_admins + sa_* RPCs |
Landing (/) -> Sign up (/auth?signup=1)
| |- auth.users row created
| |- trigger: profiles + personal company + company_members(owner)
| |- trigger: trial license issued
v
Dashboard (/) -> Settings: business profile, GSTIN, logo, brand colours,
| invoice template + font, industry packs, custom fields,
| POS / e-way / loyalty / WhatsApp / restaurant settings
v
Masters -> Parties (customers & suppliers, opening balances, credit limits)
| Items (HSN, units, prices, opening stock, barcodes, images)
| (or Excel/CSV import from the Items/Parties pages)
v
Transaction -> Sales -> New Invoice (or POS, or Restaurant table, or Store order)
| |- pick party, add lines (search / picker / barcode / voice)
| |- GST auto-split by state, charges & discounts, round-off,
| loyalty redemption, e-way block, custom fields
| |- Save -> invoices + invoice_items, stock decremented,
| loyalty points posted, activity_log entry
v
Share -> Print (A4/A5/thermal/designer) | PDF | WhatsApp | Party portal
v
Collect -> Payment In (links invoice, updates amount_paid + status)
v
Analyse -> Dashboard tiles, Alerts bell, Reports (18) -> View / PDF / Excel / Share
v
Comply -> GSTR-1 / 2 / 3B / 9 exports for the CA
v
Protect -> Backup & Restore (.bkp) and continuous cloud sync
Estimate (documents.type = estimate)
-> customer approves
-> Sale Order (stock reserved conceptually, not deducted)
-> Delivery Challan (goods dispatched)
-> Convert to Invoice (invoices + invoice_items, stock deducted)
-> Payment In (partial or full; status paid / partial / unpaid)
-> Party Statement / Outstanding report
Purchase Order (documents.type = purchase_order)
-> Purchase Invoice (stock incremented, ITC captured)
-> Payment Out
-> Purchase Return (debit note) if goods are rejected
-> GSTR-2 / GSTR-3B input tax
Reservation (optional) -> Table occupied on the floor plan
-> Order taken on the table -> KOT sent to Kitchen Display
-> KOT marked ready / served
-> Table bill -> invoice (pos_sale = true) + thermal receipt
-> Payment captured, table released back to free
POS screen -> scan barcode / tap tile -> qty & discount
-> loyalty points redeemed (optional) -> tender mode (cash/UPI/bank/cheque)
-> Save = invoice + stock update + auto-print receipt
-> Day Book / Sale Report reflect it instantly
Publish items -> share storefront link -> customer places order (store_orders)
-> owner reviews in Online Store / My Online Orders
-> convert to invoice -> fulfil -> payment -> reports
Owner -> Admin Panel -> invite user (create-user edge fn, seat check)
-> role + per-page permissions stored on company_members
Licence -> trial -> Billing page -> Razorpay order -> signature verified
-> extend_or_create_paid_license -> licences history + alerts before expiry
Data -> Backup & Restore (.bkp) | Excel export | REST API (section 20)
supabase/functions/api is a single-function REST gateway that exposes every business
resource and a set of aggregate report endpoints. It exists for mobile apps,
third-party integrations and load/performance testing.
https://<project-ref>.supabase.co/functions/v1/api/v1Authorization: Bearer <supabase access token> (validated inside the function)X-Company-Id header; defaults to the caller’s first companycompany_id is injected automatically on writes{ "data": ..., "meta": { count, limit, offset } }, errors as
{ "error": { message, status } }Resource paths include parties, items, invoices, invoice-items, payments,
expenses, documents, recurring-invoices, stock-adjustments,
loyalty-transactions, invoice-templates, business-profile, restaurant-areas,
restaurant-tables, kots, reservations, store-*, shared-reports, plus
read-only activity-log, members and licenses.
Aggregates: /reports/summary, /reports/gst-summary, /reports/stock,
/reports/outstanding. Composite write: POST /invoices/with-items.
Deploy with supabase functions deploy api. Full reference, query parameters,
curl examples, mobile-app guidance and a k6 load-test script:
docs/API.md.
| Document | Audience | Contents |
|---|---|---|
README.md (this file) |
Developers | Architecture, schema, flows, features, API overview |
docs/FEATURES.md |
Marketing, sales, end users | Plain-English, point-wise feature list with no technical terms |
docs/API.md |
Integrators, mobile & QA teams | REST API v1 reference, auth, examples, performance testing |
supabase/manual-migrations/README.md |
Operators | Multi-tenancy migration run book |
This project is proprietary software. All rights reserved.