Savaş DoğanAI integration · Software architecture

Multi-tenant CRM: 19 modules, more than 4,000 tests

I was technical lead on a multi-tenant CRM for the curtain and home-textile trade. Below: the architecture, how tenant isolation is actually enforced, and what a handover that another team can pick up looks like.

What was the project?

A multi-tenant CRM in which dealers run their whole process — from quote to installation — in one system. A single codebase and a single database serve many dealers, and each dealer sees only its own data. The product was built and shipped at Qolay Bilişim; my role was technical lead.

Role
Software Developer Technical Lead
Period
2025 – 2026
Module groups
19
Automated tests
4,000+
Architecture
Multi-tenant · single database, isolation by tenant id
Status
In production · shipped

What stack was used?

Angular on the front end with Ionic/Capacitor as the mobile target, Node.js/Express with Sequelize on the back end, MySQL 8 for data. Background jobs run on BullMQ over Redis, files in MinIO, push notifications via Firebase Cloud Messaging and SMS through a local provider. The whole stack runs on one virtual server under Docker Compose.

Stack and the role of each component
LayerChoiceRole
Web front endAngular · TypeScriptPrimary interface for dealer and head-office users
MobileIonic · CapacitorSecond build target of the same codebase, for field use
Back endNode.js · Express · SequelizeBusiness rules, authorisation, data access
DatabaseMySQL 8All tenant data, partitioned by tenant id
QueueRedis · BullMQReport generation, notifications, batch jobs
Object storageMinIODocuments, images and generated quotes
MessagingFirebase Cloud Messaging · SMS gatewayPush and SMS
RuntimeUbuntu · Docker Compose · nginx · CertbotSingle VPS with a three-tier network split
DeliveryGitHub Actions · container registryContinuous delivery with pinned image tags

How is tenant isolation enforced?

Isolation rests on one rule: the tenant identifier is not an optional filter, it is a mandatory part of the data layer. That rule is applied in three separate places — synchronous requests, queued background jobs and the object-storage path scheme — and in all three it is protected by tests rather than by convention.

How do schema changes work in a multi-tenant system?

Schema changes are additive, never destructive. The pattern is expand and contract: add the new field, have the code write both old and new for a period, migrate and verify the data, then drop the old field. Risking every tenant in a single migration step is not acceptable.

  1. Expand. The new column or table is added; the old one stays. This step is reversible.
  2. Dual write. The application writes both fields; reads still use the old one.
  3. Migrate and verify. Historical data is moved and the two fields are checked for consistency.
  4. Switch reads. Reads move to the new field; the old one is still written.
  5. Contract. The old field is dropped. Up to this point every step was individually reversible.

The cost of this discipline is that a change no longer lands in one go. What you get in return is a place to roll back to when a migration step fails. In a multi-tenant system that trade is always made in this direction.

What does the handover look like?

A handover is not a running system; it is a setup another team can take over. On this project that package included a Docker Compose definition with pinned image tags, a three-tier network split, encrypted backups, a health endpoint, and a written project rules file holding version numbers and domain invariants.

Three things this project taught me

  1. Isolation is an invariant, not a feature. Features get forgotten; invariants get bound to tests. The tenant-leak suite was the least-changed and most protective part of the product.
  2. The feedback list grows faster than the architecture. More than 55 change requests from the field were grouped into 19 module groups and ranked by risk. Working without that ranking means leaving the highest-risk item for last.
  3. Mobile parity must be a rule, not an intention. Unless mirroring every web change to mobile is written down as a rule, the two targets drift apart within six months.

Last updated: