Skip to content
SKSuraj Kumar

Customer relationships

Multi-Tenant CRM — Reference Build

A CRM each business configures to its own pipeline, built so one tenant’s data cannot be reached from another’s session.

  • Business software
  • SaaS
  • Multi-tenant
  • CRM
  • Next.js
Role
Sole engineer. Tenancy model, configurable schema, permission system and the whole application interface.
Work type
Self-directed build — no client
Timeline
12 weeks build effort · 2024
Status
Case study published
Client
Independent build

Build effort, not elapsed calendar time. Engagements ran alongside study and employment, so this figure is the work itself rather than the span it sat in.

Interface built in code for this case study — not a client screen capture.

01Business problem

What was actually going wrong

Businesses outgrow the spreadsheet but do not fit the CRM they buy, so they end up running their real process alongside the tool they pay for.

Who feels it

  • Businesses maintaining a shadow spreadsheet beside their CRM
  • Agencies managing pipelines for several clients
  • Sales managers whose stages do not match reality
  • Operators who need one view across multiple business units

A company’s sales process is specific. A manufacturer sells through distributors with sample approvals and credit checks. A services firm sells through proposals and scoping calls. A clinic works through referrals. Generic CRMs impose a pipeline built for none of them, so teams either bend their process to the tool or keep the real record in a spreadsheet and update the CRM for reporting. Both outcomes waste the licence.

For an agency or a group running several businesses, the problem compounds. Each client needs their own pipeline, fields and users, and separate accounts in a single-tenant tool means separate logins, separate configuration drift and no consolidated view.

The hard part of building this is not the interface. It is tenancy. Every query, every export, every background job and every uploaded file must be scoped to one tenant, and one missed scope is a data breach across customers. That risk is why most in-house attempts end up unsafe, not merely unfinished.

02Product overview

What got built

A multi-tenant CRM where pipeline stages, custom fields and roles are configured per tenant, and where tenant scoping is enforced by the data-access layer rather than remembered by each query.

Engineering notes

  • Tenant isolation enforced by the only exported data-access path
  • Typed custom field definitions driving input, validation, export and reporting
  • Permission filtering applied as query predicates, not interface conditions
  • Audit entries written in the same transaction as the change

Tenancy is the architecture, not a column. Data access goes through a tenant-scoped client created once per request from the authenticated session; that client is the only way to reach the database, and it injects the tenant predicate into every read and write. A query without tenant scope is not something a developer has to remember to avoid — there is no API surface that permits it.

Configuration is typed. Each tenant defines its pipeline stages, and custom fields are declared as typed definitions with validation rules, never stored as loose key-value pairs. That means a field marked as a required currency value on the quote stage is validated on the server, rendered with the right input, exported with the right formatting, and reportable — none of which works with an untyped extras object.

Permissions are role-based with record-level ownership rules layered on top, so a regional manager sees their region, a rep sees their accounts, and an owner sees everything. Permission checks run server-side on the query, meaning a hidden record is absent from the response, not merely hidden in the interface.

03Key features

What the software does, feature by feature

10 capabilities. Every one of them is deterministic software — there is no model in this build, and nothing here is described as AI.

  • 01

    Configurable pipelines

    Each tenant defines its own stages with entry requirements, so the board matches how the business actually sells.

  • 02

    Typed custom fields

    Fields are declared with a type, validation and placement, then rendered, validated, exported and reported on consistently.

  • 03

    Tenant-scoped data access

    Every read and write goes through a scoped client that injects the tenant predicate, so an unscoped query cannot be written.

  • 04

    Role and record permissions

    Roles define capabilities and ownership rules define visibility, both applied in the query and never in the interface.

  • 05

    Companies, contacts and deals

    Related records with a shared activity timeline, so a conversation with a contact is visible on their company and its deals.

  • 06

    Activity and note history

    Calls, meetings, emails and notes recorded against records with author and time, forming the account history.

  • 07

    Quotations

    Line-item quotes with tenant-specific tax handling, versioning and a PDF output, generated from deal data.

  • 08

    Saved views

    Filters, columns and sort saved per user and shareable within the tenant, encoded in the URL so a view is a link.

  • 09

    Import with mapping

    Spreadsheet import with column mapping, validation preview and per-row error reporting before anything is written.

  • 10

    Audit log

    Record changes, permission changes and exports logged per tenant, so a shared platform can answer for itself.

04User flow

How a person moves through it

The path from the trigger to the finished record, with each step attributed to whoever performs it — a person or the system.

  1. 01OwnerConfigures the tenant: pipeline stages, custom fields, roles and users.
  2. 02RepSigns in and sees only their tenant, with visibility limited by their role and ownership.
  3. 03RepCreates a company and contact, or imports a list with column mapping and a validation preview.
  4. 04RepOpens a deal, logs activity and advances it through stages that enforce their entry requirements.
  5. 05RepBuilds a quotation from the deal’s line items and sends the generated document.
  6. 06ManagerWorks from a saved view of their region and reviews pipeline by stage and owner.
  7. 07OwnerReviews the audit log for record changes, permission changes and exports.

05Architecture

How it is put together

The processing path first, then the layers it runs on, then the constraints that shaped both.

Path through the system

6 stages

  1. 01

    Authenticate

    Session resolves user, tenant membership and role.

  2. 02

    Scope

    Tenant-scoped data client constructed per request; unscoped access has no API.

  3. 03

    Authorize

    Role capabilities and ownership rules applied as query predicates.

  4. 04

    Validate

    Typed custom field definitions validated server-side on write.

  5. 05

    Record

    Change written with an audit entry in the same transaction.

  6. 06

    Report

    Aggregations run within tenant scope; exports logged.

Layers

Interface
Pipeline board with configured stagesRecord detail with dynamic custom fieldsData table with saved viewsQuotation builderTenant configuration screens
Application
Tenant-scoped data clientField definition registry and validatorsPermission predicate builderImport mapper with dry-run validation
Services
Session and tenant resolutionDocument generation for quotesImport parsingAudit writer
Data
Shared schema with tenant discriminator on every tablePer-tenant field definitionsPer-tenant role and stage configurationAudit log partitioned by tenant

Why it is shaped this way

  • Tenant isolation is enforced in one place. The scoped client is the only exported way to query, so isolation is a property of the module boundary, not a code-review checklist item.
  • Custom fields are typed definitions, not a JSON bag. That is what allows server-side validation, correct input rendering, formatted export and aggregation on a tenant-defined field.
  • Permission filtering happens in the query. A record a user may not see is absent from the response, because filtering in the interface means the data was already sent.

06Technical decisions

The choices that mattered, and what each one cost

Every decision here was contested by a reasonable alternative. The trade-off column is the part usually left out.

01

Use a shared schema with a tenant discriminator, accessed only through a tenant-scoped client.

Why

Schema-per-tenant makes migrations and cross-tenant reporting painful at even modest tenant counts. A shared schema is operationally simple, and the isolation risk is contained by making the scoped client the sole access path.

Trade-off

Isolation depends on that boundary holding, so the client is small, reviewed carefully, and the raw connection is not exported. A tenant needing physical separation would need a different model, and that is a deliberate limit.

02

Model custom fields as typed definitions, not a free-form JSON column.

Why

Untyped extras cannot be validated, formatted, sorted or aggregated reliably. Typing the definition means one declaration drives the input component, the server validator, the export formatter and the report.

Trade-off

Adding a new field type is real work — a validator, an input and a formatter. That cost is paid once per type rather than once per tenant.

03

Encode table state — filters, sort, columns, page — in the URL.

Why

A view a colleague can be sent is worth more than a view a user rebuilds each morning, and URL state also makes server rendering and caching straightforward.

Trade-off

URLs get long and parameter parsing needs to be defensive against hand-edited values, which is handled by validating parameters into a typed view state.

04

Write the audit entry in the same transaction as the change.

Why

An audit log written separately can miss entries exactly when they matter — a partial failure. Same-transaction writing means the log and the data cannot disagree.

Trade-off

Slightly heavier writes. On a CRM’s write volume this is not the constraint anyone should be optimising.

05

Validate imports as a dry run before writing anything.

Why

Partially applied imports are the worst outcome — the user cannot tell what landed. A dry run producing per-row errors lets them fix the file and retry cleanly.

Trade-off

Two passes over the file, and the preview state has to be held between them, which is bounded by an import size limit.

07Challenges

What was genuinely difficult

Not the setup work. These are the problems where the first implementation was wrong and had to be reconsidered.

01

Guaranteeing that no query could ever cross tenants, including in background work and exports.

Approach

The database connection is not exported from its module. The only public surface is a factory that takes a resolved session and returns a client whose methods inject the tenant predicate. Background jobs must supply a tenant id to obtain a client, so a job cannot accidentally run globally, and integration tests assert that a second tenant’s records are unreachable through every listed method.

Outcome

Isolation stopped depending on developer memory. Adding a new query cannot omit the tenant scope, because there is no way to construct a query without it.

02

Rendering and validating a form whose fields are defined by the tenant at runtime.

Approach

Field definitions compile into a schema at request time and drive both the rendered inputs and the server-side validator from the same source. Client validation is a convenience; the server rebuilds the schema from the stored definitions and validates independently, so a tampered payload fails.

Outcome

Tenants add fields without a deployment, and a field’s rules hold on the server and not only in the browser.

03

Keeping list views fast when every tenant filters on different fields.

Approach

Custom field values are stored in a typed side table indexed by tenant, field and value, so filtering on a tenant-defined field uses an index instead of scanning a JSON column. Pipeline boards paginate per stage, and counts are computed in one grouped query, never one per column.

Outcome

List and board views stay responsive as record counts grow, and the common N+1 pattern of counting each stage separately never gets written.

08Business value

What it changes for the business

Stated qualitatively on purpose. Invented percentages are the easiest thing to put on a portfolio and the easiest thing to see through.

Operational effect

  • Lets a business run its actual sales process in the system instead of maintaining a spreadsheet beside it.
  • Gives an agency or group one platform with genuinely separated client data and per-client configuration.
  • Makes account history durable and shared, so a handover does not lose the relationship.
  • Restricts visibility by role and ownership at the data layer — the condition for handing a shared platform to a whole team.
  • Produces an audit trail of record and permission changes, so a customer’s question about who changed what has an answer.

What would change at scale

  • Add database row-level security as a second enforcement layer, so isolation does not depend solely on the application boundary.
  • Move exports and imports to background jobs with progress reporting, since a large tenant’s export will outlast a request.
  • Introduce per-tenant rate limits and query timeouts, because one tenant’s heavy report should not degrade another’s interface.
  • Support tenant-level data residency for customers with regional requirements, which means separating the tenancy model from a single database instance.
  • Version field definitions so removing a field does not orphan historical values or break saved views silently.

09Interface

The screens where the work happens

Dense operational views rather than dashboards. These are used for hours at a time, so the priorities are legibility, keyboard flow and state that is never ambiguous.

Pipeline board

Tenant-configured stages with per-stage totals and deal cards showing owner, value and age.

Company record

Contacts, deals, custom fields and a merged activity timeline in one view.

Field configuration

Field definitions with type, validation, placement and whether they are required at a given stage.

Import preview

Column mapping with a per-row validation result before any record is written.

The screen above is built in HTML and CSS for this case study. It reproduces the layout, states and vocabulary of the real build without exposing client data, which is why it exists rather than a screenshot. It is evidence of design and of the decisions behind it. It is not a photograph of a deployed system, and no part of it is a capture of anyone else’s product.

10Technologies

What it is built with

Chosen for the shape of the problem, not for novelty. Anything unusual is justified in the decisions section above.

Frontend
  • Next.js App Router
  • TypeScript
  • Tailwind CSS
  • Server Actions
Application
  • Tenant-scoped data client
  • Runtime schema compilation
  • Permission predicate builder
  • URL-encoded view state
Data
  • PostgreSQL
  • Indexed custom field values
  • Transactional audit log
Platform
  • Session-based tenant resolution
  • Document generation
  • Spreadsheet import

11Technical preview

Structure, models and annotated excerpts

There is no repository link on this site. What is available instead is the module structure, the data model, the interface contracts and annotated excerpts written for this case study — enough for a technical reviewer to judge the engineering.

Technical preview

Implementation detail for this build

Selected implementation details are available for technical review: component structure, data models, architecture notes and sanitized code excerpts. Some client-specific source material stays private because of confidentiality. Enter the project access keyword if you have been given one — the same one works across every case study.

A presentation convenience, not authentication. This is a static site, so the keyword ships to your browser with the page and anyone reading the bundle can find it — worth saying plainly rather than dressing it up. Nothing confidential is stored behind it.

Inside this section

  • Module and folder structure for the build
  • Data models and the interface contracts between layers
  • 2 annotated implementation excerpts
  • Design notes covering the decisions the excerpts imply
  • The same keyword opens every case study on the site

For private client work, contact me for a walkthrough.

Next step

Need something along these lines?

Send the process, the constraints and the deadline. You will get an honest scope, an architecture sketch and a timeline before any commitment.

Email
surajk86808@gmail.com
Based in
Bengaluru, India
Working hours
IST (UTC+5:30)
Availability
Taking new engagements