The context
A general renovation company — building work, finishing trades and energy renovation — was running its operations on a stack of generic tools: spreadsheets for worksites, email for sales follow-up, separate documents for quotations. Yet the business involves a long pipeline, heavily regulated on part of the work: a file moves from lead qualification to technical visit, then costing, delivery, and administrative follow-up.
The goal was to replace that patchwork with a single system where every file advances stage by stage, where field sales staff, technicians and back-office work on the same data, and where calculation rules can change without redeploying the application.
What I built
Lead simulator and management
upfront qualification, conversion into client then project, with a full history of commercial actions.
Technical visits with dynamic forms
configurable form templates per worksite type and trade, filled in the field, with no development needed for each new requirement.
Quotation engine
costing by work type and trade, with parameterised calculation of public-aid schemes for the projects that qualify, and every amount traceable.
Financial chain
payments, schedules, analytical accounting and automatic tiered commission calculation for sales teams.
Logistics
products, multi-warehouse stock, equipment, vehicles and suppliers.
Built-in communication
multi-account WhatsApp messaging attached to files, realtime notifications, transactional email.
Steering
dashboards, Excel exports, PDF generation of contractual documents, complete activity log.
Architecture & technical decisions
The back-end is organised into around thirty isolated business domains (a DDD approach): each domain — quotations, subsidies, payments, stock, technical visits — owns its models, services and rules. On an application this broad, that separation is what lets the subsidy logic evolve without risking a regression in billing.
I chose a hybrid GraphQL + REST API: GraphQL for reads, because every dashboard needs a different combination of data and this avoids an ever-growing list of endpoints; REST for writes, where strict validation and business side-effects are easier to read and to secure.
Persistence is polyglot: MySQL for everything transactional and relational (files, payments, stock), MongoDB for what is structurally variable — the answers to dynamic technical-visit forms and the activity logs, whose shape changes with the form template.
WhatsApp messaging is isolated in a separate Node service that talks to the main API. This kind of integration is inherently unstable (sessions, reconnections): isolating it means a disconnection never affects the CRM itself.
Technical challenges
Moving regulatory rules. Subsidy scales change regularly. Encoding them in code would have meant a deployment for every change: they are modelled instead as versioned, parameterised data, with history retained so older files can still be recalculated exactly as they were.
Forms designed without a developer. The technical-visit engine is built on form templates with answer storage decoupled from the schema — adding a new field type requires no database migration.
Fine-grained permissions. Sales, technicians, admin staff and management see different slices of the same files: granular roles and permissions, enforced server-side and mirrored in the interface.
Realtime without noise. Notifications go through a central orchestration layer over WebSockets, so modules don't each emit their own events inconsistently.
A reproducible environment. Everything is containerised with Docker (API, WhatsApp service, queues), guaranteeing identical behaviour from local development to production.