GROWVANTA Engineering overview

A real-estate CRM that runs anywhere PHP does.

No framework, no package manager, no build step, no third-party libraries. Upload it to ordinary shared hosting and it works — because that is where the customers who need it already are.

51
Database tables
29
API route files
177
Automated tests
0
Third-party packages
The problem

Enquiries arrive in six places and are followed up in none

A property team hears from buyers through portals, its own website, Facebook and Instagram ads, WhatsApp, walk-ins and the phone. Each channel has its own inbox. None of them talk to each other.

Leads go cold in an inbox

An enquiry sits in an ad account nobody opens until Friday. By then the buyer has visited two other sites.

Nobody can prove attribution

Which portal produced bookings, not just enquiries? Without one record per lead, that argument is settled by whoever speaks loudest.

Everyone sees everything

Spreadsheets have no permissions. A telecaller can read the whole pipeline, and a departing executive can take it with them.

Generic CRMs solve a generic problem. Property sales turn on a site visit, a RERA number, a possession date and a configuration — fields a generic tool makes you invent as custom text.
What was built

The capability map

Lead management

Capture, scoring out of 100, an 11-stage pipeline, duplicate detection and merge, activity timeline, follow-ups, tags and custom fields.

Project inventory

RERA number, developer, price and area bands, configurations, amenities, possession date, image gallery — with per-project reporting.

Assignment engine

Round robin, least-loaded-first and team round robin, driven by rules on source, project, city or hour of day.

Permissions

23 modules × 89 distinct actions, 9 ready-made roles, 5 visibility scopes, plus per-user grants that need no new role.

Messaging

WhatsApp via the Meta Cloud API and email over raw SMTP, both governed by a three-state readiness check, with a delivery log that records why anything was skipped.

Capture

Embeddable and hosted web forms with origin allow-listing and a honeypot, Meta Lead Ads webhooks, and bulk import with duplicate preview.

Insight

Dashboards, source and funnel reports, targets, per-person performance, saved views, and XLSX export of any list on screen.

Operations

Full audit log, in-app notifications, staff email alerts on new leads and upcoming follow-ups, and a cron sweep for time-based work.

The stack

Chosen for where it has to run

The target is a ₹300-a-month cPanel account, not a Kubernetes cluster. That single constraint decided almost every technical choice below.

Frontend A single-page app in plain ES modules — hash router, no framework, no bundler, no build step. Edit a file, refresh the browser. 38 modules · 12k lines
API A REST surface returning a consistent {data, meta} or {error} envelope, routed by .htaccess into one front controller. 29 route files
Domain Services holding the business rules — leads, projects, assignment, scoring, comms, provisioning — kept out of the route handlers. PHP 8.1+
Data MySQL 5.7 / MariaDB 10.3 and up, reached only through PDO prepared statements. A base schema plus ordered migrations, so an existing install upgrades. 51 tables · 3 migrations
Hosting Apache or LiteSpeed shared hosting. Seven standard PHP extensions and nothing else. cPanel / hPanel
pdo_mysql openssl mbstring fileinfo json zlib curl — optional
No Composer. No npm. No vendor directory. Every dependency is a future upgrade someone has to perform on a server they may not have shell access to. The libraries that would normally be pulled in were written instead — and are listed below.
Engineering decisions

The parts worth explaining

Where the build departs from the obvious approach, and why.

Authorization has two gates, not one

Every request asks two separate questions: may this role perform this action, and is this record inside the part of the organisation this person can see? Visibility walks the manager and team tree in code rather than a recursive query, so MySQL 5.7 is still supported.

A channel is never green just because it is on

WhatsApp and email report red, amber or green. Green requires a test send that actually landed. The server re-derives the verdict before every message, and a skipped message records its reason in words.

XLSX written by hand

Spreadsheet export builds the ZIP container itself using gzdeflate, so no ext-zip and no PhpSpreadsheet. Values go in as inline strings, which also makes CSV-style formula injection inert.

SMTP spoken directly

A small client handles STARTTLS, implicit SSL, AUTH LOGIN and dot-stuffing. It is a few hundred lines against a library plus its transitive tree, and it fails with an error a support person can read.

Work continues after the response

Domain events queue during a request and flush after litespeed_finish_request(), so sending a welcome message never makes the user wait for a third-party API.

Secrets are reversible only with the key

Integration credentials are sealed with AES-256-GCM derived from APP_KEY. Passwords are bcrypt and one-way. Session tokens are opaque and stored only as a SHA-256 hash.

Anatomy of a request

What happens when a form on your website is submitted

1
The browser posts to the public capture endpoint
No authentication — this is the front door for website traffic.
2
Four checks run before anything is written
Per-form origin allow-list, per-IP rate limit, honeypot field, and the rule that a submission may only write the columns that form has explicitly mapped.
3
The lead is created against the form's own organisation
Source and project come from the form, never from the visitor — so scoring and reporting see the real attribution on the first evaluation.
4
Duplicates are matched, scored and assigned
Phone and email are checked against existing buyers; the assignment rules pick an owner; the scoring rules set a band.
5
The response returns — then the rest happens
Welcome message to the buyer, alert to the owner, audit entry and webhooks all run after the connection closes.
How it is kept honest

Tests that drive the real HTTP API

Not mocked units. The suites sign in over HTTP, act as different roles, and assert what each one is allowed to see.

177 assertions

Multi-company isolation, hierarchy and assignment, project visibility, lead permissions, and the staff alert rules — each suite cleans up after itself.

Static checks

Column names verified against the live schema, NOT NULL coverage on inserts, JavaScript import resolution, and dead-code detection across the front end.

Reproducible releases

One script stages the files, exports the data in dependency order, refuses to package if a secret is present, and writes a dated archive.

A negative test is the only proof that a guard works. Every permission rule is asserted twice — once that the right role can act, and once that the wrong one is refused with the correct status.

Built to be handed over, not just delivered.

One database the customer owns, on hosting in their name, with an upgrade that is a file copy and a migration — no build server, no container registry, and no dependency that expires.

PHP 8.1+ & MySQL 5.7+ Zero third-party packages Deploys by upload 177 tests green