Schema-per-tenant, deliberately
Every client gets their own PostgreSQL schema — biz_XXXXXXXX — inside one shared database, with the public schema reserved for the platform layer. Each authenticated request runs a fixed pipeline: auth decodes the JWT into a user, business, schema and role; a subscription guard blocks suspended or expired accounts; then the tenant resolver sets the search path for that connection. I chose this over a shared table keyed by a business_id column for one reason — in the shared-table design, a single forgotten WHERE clause leaks one business's books to another. With isolation enforced at the connection level, safety is the default rather than a discipline. An automated cross-tenant test proves it: Client A's token asking for Client B's data must return 403, and that test never gets deleted.
Onboarding measured in seconds
Signup is entirely self-serve. Creating a tenant validates the GSTIN, generates a schema name, writes the platform record, creates the schema, migrates it, then seeds it — five RBAC roles, the textile HSN range, invoice templates, product categories — before creating the owner account, setting up recurring billing and sending a welcome message. Deletion is the mirror image: a soft delete with a 30-day retention window before the schema is dropped, so nobody loses their books to a misclick.
Built for the counter, not the demo
GST is handled properly — CGST plus SGST intra-state, IGST inter-state, derived automatically rather than picked from a dropdown. Every invoice carries a UPI deep link rendered as a QR on the PDF and as an ESC/POS QR on the thermal receipt, so GPay, PhonePe, Paytm, BHIM and JKPay all just work. 80mm thermal printing is treated as a must-work path because it is how these shops print. Money is INR-first with export currencies, customer credit follows the khata model shops already run, and every financial record is soft-deleted — an invoice or ledger row is never truly destroyed. Time is stored in UTC, displayed in IST as DD/MM/YYYY, with a single sanctioned date utility so nothing drifts.
Security is the product
Holding many businesses' commercial data in one system makes security a feature rather than a phase. Access tokens are short-lived and kept in memory; refresh tokens rotate, are single-use, and live in HttpOnly, Secure, SameSite=Strict cookies. TOTP 2FA is mandatory for admins, passwords are bcrypt at cost 12, and logins are throttled by IP. Payment keys, bank details and TOTP secrets are AES-256 encrypted at rest; queries are parameterised through Prisma with no raw interpolation; Helmet sets CSP and HSTS; CORS allow-lists are environment-scoped. Plan limits are enforced at the API, never merely hidden in the UI, and four tiers of Redis rate limiting sit in front of it all.
A fix worth writing down: instant logout
While hardening auth I found that signing out revoked the refresh token but not the access token — so a 15-minute access token could keep opening routes after sign-out, from another tab, the back button, or any lingering client. The Super Admin middleware had no revocation check at all. I wired sign-out into the same immediate-revocation mechanism used for account deactivation: on logout the user's live access tokens are revoked in Redis, and both the tenant and Super Admin middleware reject any token minted before that instant. The next request after sign-out returns 401 — everywhere, immediately, not fifteen minutes later — covered by unit tests and a real-PostgreSQL integration test that logs in, signs out, and asserts the old token is refused.
The economics of one platform
The architecture and the business model are the same decision. Because one platform and one database serve every tenant, infrastructure barely moves as clients are added, which is what makes the subscription pricing viable at a target of 1,000 tenant businesses. Usage-based third-party services — WhatsApp, AI calling, payment gateways — run on each client's own provider credentials and are billed to them directly by those providers, so I never resell metered messaging. That keeps the margins clean and the compliance surface small. Alongside the managed cloud edition there is an on-premise licence, sold manually and tracked separately, for shops that want everything on their own hardware. Kapda Stock is designed, architected and built by me, in collaboration with CodeMites.
What I took away
Isolation should be the default, not a discipline — schema-per-tenant means a forgotten filter cannot leak data across businesses. Auth is a lifecycle, not a login; the interesting bugs live in refresh rotation, revocation and sign-out, not the password check. And local reality wins: thermal printing, UPI QR, GST state logic, IST dates and Kashmiri/Urdu RTL aren't extras here, they're the difference between software a shop adopts and software it ignores.