A multi-agency CRM for roofing companies
Estimates, scheduling, payments and field measurements in one shared real-time view, per agency.
The problem we were handed.
Roofing agencies juggled estimates, scheduling, payments and field measurements across disconnected tools, with no shared real-time view.
A single roofing job crosses more systems than it looks like it should. A rep pulls an aerial measurement, someone turns that into an estimate, someone else raises a material order against it, a crew gets scheduled, the homeowner pays, and eventually it all has to land in the books. Every one of those steps lived in a different product, and the handoffs were people re-keying numbers.
The platform also had to serve many independent agencies at once — separate crews, separate price books, separate staff, separate customers. One agency seeing another's pipeline was not an inconvenience, it was the end of the product. But the operator still needed a view across all of them.
Then there's weather. Roof work is planned around a forecast, and a bad forecast doesn't cancel one job — it cascades through the week, the crew roster and the material deliveries behind it. Rescheduling by phone call was the default, and it was where the day went.
How we built it.
A multi-tenant CodeIgniter platform with per-agency panels and RBAC, JWT-secured APIs, ReactPHP/WebSocket messaging, and deep integrations — Hover, Beacon, Twilio, Stripe, QuickBooks and weather-aware Google Calendar scheduling.
Per-agency panels and RBAC
A multi-tenant model giving each agency its own panel, roles and data boundary. Tenancy is enforced below the application layer, so a forgotten filter in a new feature can't leak another agency's jobs. Roles were modeled on how a roofing business actually runs — owner, sales rep, crew lead, back office — rather than a generic admin-or-user split.
- Tenant scoping enforced at the query layer, not per controller
- Role matrix built around real field and office responsibilities
- Per-agency price books, branding and document templates
- A platform-level view for the operator, with no cross-tenant bleed
One JWT-secured API surface
Web panels, field clients and vendor callbacks all speak to the same API rather than three near-copies of it. Tenant and role claims ride inside the token and are verified on every request, so authorization isn't something a client can talk its way around. Tokens are short-lived with refresh — a phone left on a job site is a nuisance, not an incident.
- One contract for every client, versioned and documented
- Tenant and role claims carried in the token, checked per request
- Short-lived tokens with refresh, and revocation that actually works
Real-time messaging on ReactPHP
A long-running ReactPHP process holds WebSocket connections for chat and alerts between the office and the crews. Threads are scoped to the job, so context stays with the work instead of scattering across personal inboxes and text messages. Where the socket can't hold — and on a roof, it often can't — messages queue and replay, with Twilio SMS as the fallback path for anything urgent.
- Job-scoped threads so the history stays attached to the job
- Live alerts on estimate approvals, payments and schedule changes
- Offline-tolerant queue and replay for patchy field connections
- Twilio SMS fallback when a crew is off the network entirely
Weather-aware scheduling
Crew calendars sync two ways with Google Calendar, so nobody has to keep a second schedule to be sure. Forecast data is checked against the job site's location rather than the office, and at-risk days surface before the crew is already loading the truck. When a day has to move, the system proposes slots that respect crew availability and material delivery dates instead of just clearing the slot.
- Two-way Google Calendar sync per crew and per agency
- Forecast checks against the job address, not the branch
- Reschedule proposals that account for crew load and deliveries
Integrations behind adapters
Hover for aerial measurements, Beacon for materials, Twilio for messaging, Stripe for payments, QuickBooks for the books, Google Calendar for scheduling. Each sits behind its own adapter with its own retries, backoff and error surface, so a vendor outage degrades one feature rather than the application. Webhooks are queued and idempotent — a replayed event doesn't produce a second charge.
- Hover measurements flowing straight into estimate line items
- Beacon material orders raised from the approved estimate
- Stripe payments reconciled into QuickBooks with no manual export
- One adapter per vendor, so swapping one is a contained change
Architecture & integrations.
What it delivered.
Multi-tenant is easy to demo and hard to keep honest — the second agency is where the shortcuts show. We built the boundary first and hung the features off it, which is why onboarding an agency stays a configuration task and adding a vendor stays an adapter. The integration surface is the part of a system like this that ages fastest. It's also the part we left documented.
Questions this project raises.
Usually, yes. This build wired in Hover, Beacon, Twilio, Stripe, QuickBooks and Google Calendar — each behind its own adapter so one vendor change never ripples through the app.
Yes — it's a stack we've run for years. We read the codebase and price against what's there, then modernize in steps you can ship: CI3 to CI4, or CodeIgniter to Laravel. No big-bang rewrite, no frozen roadmap while it happens.
Tenancy is enforced below the application layer, so a forgotten filter in a new feature can't leak another agency's jobs, and tenant and role claims ride inside the token and get checked on every request. The operator still gets a view across all of them.
Messages queue and replay when the socket comes back, and Twilio SMS is the fallback for anything urgent. Field connectivity was a design input rather than a bug found later — on a roof, the socket often can't hold.
Project-based with milestone billing suits a defined scope; a dedicated monthly team fits better when the roadmap keeps moving. Architecture and API contracts get settled before code, then short sprints — you look at working software early, not a status deck.
Related work.
Have a problem shaped like this one?
Tell us what's in the way. You'll hear back from a senior engineer within one business day.