FullServicePro: Restoration CRM with AI, In Flight
A white-labeled Frappe + Vue restoration CRM for a franchise operator.
Built around the way restoration franchises actually run: insurance-driven billing, multi-location crew dispatch, and an inline-edit experience that respects the way office and field teams really work. AI agent and in-app chatbot land next.
Industry: Multi-location restoration franchise operator, the kind of business that runs water, fire, mold, and contents jobs across dozens of locations, billed predominantly through insurance carriers and adjusters.
Why custom: Off-the-shelf restoration platforms (ServicePro, Albi, and the rest) charge per user per month and ship a fixed data model. For a franchise operator scaling crew headcount, the bill is the wrong shape and the workflow is the wrong fit.
Engagement: Long-running build with weekly demos. Designed to be handed off to an in-house team when the client's ready, or to run as managed services indefinitely. No platform lock-in. The source is theirs.
Why off-the-shelf restoration software keeps falling short.
Restoration franchises have data models that off-the-shelf platforms can't bend to: multi-location with shared customers, insurance-driven billing with state-by-state rules, mobile-first crews who don't sit at desks.
Six rules that shaped the build.
Frappe for the data model
Python-native DocTypes give us schema we can shape directly. Restoration's data is multi-location, insurance-driven, and mobile-first, and it fights every off-the-shelf platform. Frappe doesn't.
Vue 3 SPA for the UX
We're not living inside Frappe's default desk UI. A separate Vue 3 + Vite + Tailwind frontend gives us control over every inline-edit affordance, every dark-mode tile, every keyboard shortcut.
Three-tier API, always
Thin @frappe.whitelist() handlers call service-layer functions that contain the business logic. The DB is the only thing the service layer touches directly. Tests stay focused; refactors stay safe.
Composables over copy-paste
useApi, useForm, useTable, useToast, useModal: every page consumes them, no page reimplements them. New screens take hours, not days.
Nothing hardcoded
All enums, badge styles, dropdown options, and theme values live in constants/ and CSS custom properties. A status-label rename never requires touching pages.
Permissions at the DB layer
Frappe RBAC plus explicit permission checks at every API call. Never ignore_permissions=True without a documented reason. Audit logs on every destructive action.
Eight modules. One coherent system.
Lead pipeline
FSP Lead with status workflow, source tracking, assignment, and conversion-to-Account/Job. Filters and saved views built in. No per-user CSV exports.
Accounts & Contacts
FSP Account and FSP Contact linked through the LinkLookup primitive with inline edit, Ctrl-click to new tab, and quick-create from any form. The book of business stays in one place.
Job dispatch
FSP Job with status lifecycle, crew assignment, service-line breakdown, attachments, activity timeline. Mobile-first crew views ship next.
Service catalog
FSP Service covers restoration service lines (water, fire, mold, contents) with pricing rules. Reusable across leads, jobs, and invoices.
Activity timeline
Every record carries an immutable activity log. Notes, status changes, attachments, and system events show up as one chronological feed per Account, Lead, and Job.
Calendar
Dispatch calendar showing scheduled jobs across crews. The view ops actually opens in the morning.
Financial reports
Five live reports: Financials, Outstanding (AR aging), Jobs, Leads, Team. Built on the service layer, paginated at the DB level. No Python-side filtering of result sets.
AI agent + chatbot, in flight
Active development. Lead-intake agent for first-notice-of-loss calls, plus an in-app chatbot for crew status updates and office Q&A over the Account / Job graph. Shares the agentic patterns from our other AI builds.
Why it feels good to use.
A franchise CRM lives or dies on small affordances: how fast you can find a contact, whether you have to leave the page to fix a typo, whether you can open a related job in a new tab the way you would on any modern site. These are the details we agonize over.
LinkLookup: inline edit, navigation, and quick-create in one primitive
Every reference to another DocType (Account, Contact, User) uses the same component. Hover for a pencil icon to edit in place. Ctrl/Cmd+click opens the linked record in a new tab. "+ New" opens a modal that creates the related record without losing your current form state. Used on every detail page and form across the app.
Saved filter views per list
Backed by saved_filter_service and list_view_service. Power users carry their own working set across sessions. No CSV-as-state.
Dark mode from day one
[data-theme="dark"] selector with CSS custom properties. Branded for the franchise, not a forced palette.
Standardized response envelopes
success_response, error_response, paginated_response, validation_error_response. Every API call returns the same shape. Frontend composables can rely on it.
Salesforce import path
import_salesforce.py: a documented migration route for franchises moving off Salesforce or other legacy CRMs. The architecture sprint includes a data-mapping deliverable.
Four decisions that earned their footprint.
Why Frappe over Salesforce
Salesforce's per-user pricing kills the unit economics for a franchise with 50+ crew. Frappe is open-source, self-hostable, and has a Python-native domain model that bends to restoration's data instead of fighting it.
Why Vue 3 over Frappe Desk
Frappe's default desk UI is built for back-office data entry. Restoration franchise ops need a customer-facing-grade interface with inline edit, fast list views, and a branded look. A separate Vue SPA on top of Frappe's API gives us both.
Why a separate service layer
Business logic doesn't belong in @frappe.whitelist() handlers, where it can't be tested without HTTP. Service functions are pure Python: easy to test, easy to compose, easy to expose through admin scripts later.
Why composables over Pinia for everything
Pinia owns auth + settings (cross-cutting global state). Data fetching, forms, tables, and toasts each have one composable that pages call. State stays local where it should be.
What lands next.
- Crew mobile views: offline-first job updates, photo uploads from site
- AI lead-intake agent (FNOL): handles inbound calls, fills the lead record
- In-app chatbot: "what's the status of job FSP-JOB-00214?" answered without leaving the page
- Invoicing module: ACORD-aware paperwork, carrier-specific billing, Xactimate-format export
- Franchise HQ dashboard: throughput, margin, and utilization by location
- Carrier portal integrations where carrier APIs exist
What it runs on.
| Layer | Technology |
|---|---|
| Backend | Frappe 15 · Python · Service-layer architecture |
| DocTypes | FSP Lead · FSP Account · FSP Contact · FSP Job · FSP Service · FSP Claim |
| Frontend | Vue 3.5+ (script-setup) · Vite 6 · TypeScript · Pinia |
| UI | Tailwind CSS 3.4 · frappe-ui · Feather Icons · CSS custom properties |
| Data access | useApi · useForm · useTable composables · standardized response helpers |
| Reports | 5 reports: Financials · Outstanding · Jobs · Leads · Team |
| AI (in flight) | Lead-intake agent · in-app chatbot · activity-graph Q&A |
| DevOps | pre-commit (ruff · eslint · prettier · pyupgrade) · Frappe bench |
Restoration practice
How we approach restoration franchise CRM builds, beyond just FullServicePro.
SirenAI: AI Booking Platform
Multi-channel AI agent rebuild. Patterns we'll carry into the FullServicePro AI work.
Dear Brightly: Telehealth Platform
Another platform rebuild: modular monolith, unified admin, 60k+ users migrated.
Running a restoration franchise on rented software?
If ServicePro fees scale faster than your margins, or your billing flow doesn't match ACORD/Xactimate reality, that's where this kind of build pays back. Start a conversation. we'll tell you honestly whether the ROI works at your scale.