Skip to content
Engineering Atlas
SystemsChartered Accountant CRM — Multi-Role Practice Management

Chartered Accountant CRM — Multi-Role Practice Management

Independently architected the entire frontend and ~70% of the backend for a multi-role CRM serving a Chartered Accountant practice, with role-based access control and an end-to-end Razorpay payment pipeline.

Status
Archived
Timeline
Apr 2023 – Dec 2025
Reading Time
5 min
Domains
Full-Stack Architecture, Payments, Access Control
Technologies
React.js, Redux, TypeScript, NestJS, TypeORM, SQL, Razorpay
Contents

Executive Summary

A multi-role CRM built for a Chartered Accountant practice, supporting Admin, Relationship Manager, and Client roles across onboarding, document processing, filings, and workflow tracking. Independently architected and built the entire frontend, and owned roughly 70% of the backend — designing the relational schema, implementing JWT authentication and role-based access control with NestJS Guards, and structuring validation layers around every request. Also integrated Razorpay end to end, from order creation through webhook-verified payment confirmation and reconciliation, so payment status in the CRM could be trusted rather than assumed.

Business Context

Problem
A Chartered Accountant practice needed a single system to manage three distinct kinds of users — internal Admins, Relationship Managers, and Clients — through client onboarding, document processing, filing workflows, and payment collection, with each role restricted to the CRM surface it was actually entitled to use, and with payments verified rather than trusted at face value.
Users
Admin staff, Relationship Managers, and Clients of the CA practice, each scoped to a different set of CRM modules and actions.
Business Goals
Give each role only the access it needs, enforced consistently on both frontend and backend. Let clients pay for services online with a payment record that can be trusted. Keep the schema extensible as filing and workflow states evolved.
Success Metrics
Role restrictions in the UI never drift from what the backend actually permits. Payment status in the CRM only changes after Razorpay's own verification, not a client-side redirect.
Environment
Production NestJS/TypeORM backend with a React/Redux frontend, used daily by the practice's Admin, RM, and Client users, processing real client payments through Razorpay.
Stakeholders
Owned the entire frontend and ~70% of the backend directly; the remaining backend work was shared with other engineers on the same schema and API surface.

Constraints

Three-role permission surface

Admin, RM, and Client roles each needed different CRM access. UI restrictions and protected routes had to stay aligned with backend RBAC policies rather than becoming a second, independently-drifting source of truth.

Payment integrity

Razorpay order creation, verification, and webhook handling had to be trustworthy end to end — payment credentials and verification signatures needed secure handling to prevent fraud, not just a successful-looking client redirect.

Partial backend ownership

Owning ~70% of the backend meant schema and API decisions had to stay consistent with the remaining 30% built by other engineers, without a single owner to unilaterally resolve every design question.

Data integrity across workflow states

Clients, filings, and workflow states moved through multiple stages. Validation layers and DTO pipelines had to hold data integrity at every request, not just on initial creation.

System Blueprint

The frontend talks to NestJS REST APIs guarded by JWT auth and role checks, which read and write a relational schema covering clients, filings, and workflow state, with a separate Razorpay integration path for order creation, verification, and reconciliation.

Frontend

React.js application with Redux-managed global state, rendering role-scoped views for Admin, RM, and Client users and enforcing protected routes aligned with backend RBAC.

React.jsReduxTypeScript

API Layer

NestJS REST APIs guarded by JWT authentication and NestJS Guards for RBAC, with DTO validation pipelines on every request.

NestJS

Data Layer

Relational schema for clients, filings, workflow states, and interactions, managed through TypeORM, plus invoice and transaction records for reconciliation.

TypeORMSQL

Payments

Razorpay integration handling order creation, payment verification, and webhook-based confirmation, kept in sync with internal CRM payment records.

Razorpay
  • Frontend (Redux + Axios) → NestJS REST APIs (JWT + RBAC Guards) → TypeORM/SQL data layer
  • Razorpay checkout → webhook → signature-verified payment confirmation → CRM invoice and reconciliation records

Architecture

Frontend ownership across three roles

The entire frontend was architected and built independently, with role-based UI restrictions and protected routes for Admin, RM, and Client roles kept aligned with backend RBAC policies rather than treated as a separate permission model.

Backend RBAC as the enforcement layer

Access control is enforced by NestJS Guards on the backend, with JWT authentication securing every request. Frontend role restrictions shape what a user sees, but the Guards are what actually decide what a request is allowed to do.

Relational schema for CRM workflow state

Clients, filings, and workflow states are modeled relationally in SQL via TypeORM, with validation layers and DTO pipelines enforcing data integrity as records move through onboarding, document processing, and filing stages.

Razorpay payment pipeline

Payments are confirmed through order creation, webhook handling, and signature verification rather than a client-reported success state, with dedicated database structures tracking invoices, transaction history, and reconciliation.

Engineering Decisions

Trusting a client redirect vs. verifying payment via webhook

Problem
A client-side redirect after checkout can report success even when the payment didn't actually clear, or can be spoofed — it isn't a reliable signal that money moved.
Options
  • Mark a payment as complete based on the client-side redirect after Razorpay checkout.
  • Confirm payment only after Razorpay's webhook fires, with the payment signature verified server-side.
Decision
Payment status is only updated in the CRM after webhook delivery and signature verification succeed.
Trade-offs
Adds a dependency on webhook delivery and signature-verification logic that has to be maintained correctly, instead of trusting a simpler client-side flow.
Outcome
Invoice and reconciliation records only reflect payments Razorpay has actually verified, closing off a straightforward fraud vector.

Where role restrictions are the source of truth: UI or backend

Problem
With three roles (Admin, RM, Client) sharing one CRM, restricting access only in the frontend would leave the backend as an unenforced fallback — a role check missed in one component could expose data the role shouldn't reach.
Options
  • Restrict access primarily through frontend route guards and conditional rendering.
  • Enforce access control at the backend with NestJS Guards, and treat frontend restrictions as UX rather than security.
Decision
Backend NestJS Guards are the enforcement boundary; frontend role-based UI restrictions are aligned to them but not relied on for security.
Trade-offs
Every new role-scoped feature needs a backend Guard in place before the frontend restriction means anything, adding a coordination step the frontend alone can't skip.
Outcome
A missed or bypassed frontend check can't expose data a role isn't entitled to — the backend still rejects the request.

Production Stories

This section is planned but not yet documented.

Lessons Learned

  1. Frontend role restrictions are a UX layer, not a security boundary — the backend Guard is what actually has to hold.

  2. Verifying payments through a webhook and signature check, instead of trusting a client redirect, is the difference between a payment record you can trust and one that's a guess.

  3. Owning a majority share of a shared backend still means designing schema and API changes so the remaining owners can build against them without renegotiating the model each time.



Command Palette

Search for a command to run...