One login for a solar business, in place of a patchwork of tools
CRM, projects, quotes, purchase orders, outreach, a customer portal and an AI copilot in one connected system, with a pipeline that refuses to advance a deal until the data to deliver it exists, and a team-test loop that has shaped it through sixty-one tickets.

- data models, one connected system
- 206
- API endpoints
- 800+
- feedback tickets from the team test
- 61
The problem
The business ran on a patchwork: a CRM, a monday.com workspace, spreadsheets and several small apps built on a no-code platform (one for purchase orders, one for quotes, one for marketing). Each was reasonable alone. Together they meant a deal was re-keyed between tools, a quote's numbers disagreed with the table beneath them, and nobody could say which version of a customer was true.
A commercial solar project is a long chain of dependencies (grid applications, surveys, procurement, delivery, handover and aftercare), and the cost of discovering a missing meter number or half-hourly data after a quote has gone out is a re-issued quote and a lost fortnight.
The people using it are directors, sales, technical, operations and finance, each needing a different slice, with spending authority and customer data that cannot all be open to everyone.
What we built
A pipeline that names what is missing
Partial data is always saved; gaps gate the stage. Dragging a deal forward asks the API whether the deal and its sites hold what that stage needs, and a refusal says exactly what is missing: an MPAN, bills, a signed-off survey, half-hourly data, a quote. The rule is cumulative, so a deal dragged straight from discovery to won must satisfy everything in between; moving backwards, closing as lost and imported historic stages are never gated. An override exists, needs its own permission and records a reason, because a rule nobody can get past is a rule people learn to route around.
One quote engine instead of six drifting copies
The original quote tool had the same arithmetic written out six times, and the copies had drifted: a payback figure that differed between the tab and the saved record, a twenty-five-year total that disagreed with the table beneath it. They were reconciled into one module of pure functions with tests. The customer reads their quote at an unguessable no-login link, and the cost build is stripped on the server before anything leaves it.
Purchase orders that approve themselves within limits
Each person has a spend policy, and the three-step order wizard shows a live verdict on whether this order will auto-approve before it is submitted. Over-limit orders route to finance, a director or operations. Goods are received against order lines with partial deliveries tracked, invoices are matched in one click against what was ordered and delivered, and the order number series continues the legacy one rather than starting again.
A programme view that does the critical-path maths
Projects have a Gantt with multiple predecessors per task, per-link lag, true float and critical path, ring detection over the whole graph and a what-if sandbox that changes nothing until it is saved. Baselines are stored on the server, and a daily job tells the project manager which tasks have slipped against the plan.
Calls turned into commitments, with a person in the loop
Meeting transcripts are matched to a CRM record by attendee, written up against a template for the type of call, and a model extracts who said they would do what by when. Nothing becomes real until a person confirms it. Confirmed actions then escalate (a notification, a nudge near the due date, repeated nudges once overdue, an email to the owner if still undone six days on) and every step is stamped so a re-run never chases twice.
AI that goes through the same door as everyone else
The in-app copilot runs a tool-use loop over the live CRM, but its writes call the same business logic as the REST API, so gates, permissions and the audit log all still apply, and writes sit behind a flag. The same tool registry is exposed as an MCP server with an OAuth layer, so Claude itself can connect as a custom connector. The public website chatbot gets exactly one tool (capture a lead) and none of the read tools.
Imports that survive a short function time limit
Moving data from the old tools was the riskiest part, so imports are resumable cursor jobs that work in slices of about twenty-two seconds to stay inside the host's function time limit, save their position and are drained by a scheduled task. They dry-run first and cannot be applied until the dry-run has been viewed, remember a mapping against a fingerprint of the sheet's headers, park files they do not recognise rather than guess, and treat a blank cell as nobody having answered, never as an instruction to erase.
A feedback button on every page, and a rule about closing tickets
Every screen carries a feedback control that captures the route, severity and a screenshot or screen recording. A ticket cannot be closed without a written resolution, the person who raised it is emailed the outcome, and a sweep every fifteen minutes catches any close where the email failed. The system's latest change is a direct product of one of those tickets.

The outcome
Lead to aftercare now sits in one connected system: leads, opportunities, sites, quotes, projects, purchase orders, outreach, a customer portal whose stage is derived from the records rather than picked by hand, and a solar design studio. Importers brought across the previous CRM's companies, deals and contacts idempotently, so they can be re-run safely.
It is live with the client's team and being refined weekly through the in-product feedback loop, which has produced sixty-one tickets. It runs on interim hosting while the move to the client's own cloud accounts is planned.
The data model is large (over two hundred models and two hundred migrations), and the test suite is correspondingly broad, with the gates, scheduling maths and permission logic covered directly.
Honest limits: several integrations switch on only when their credentials are present, so which are live is a matter for the client's configuration; and no usage numbers or time-saved figures are claimed here, because none have been measured.
Built with
- React
- Vite
- TypeScript
- Node.js
- Express
- Prisma
- Supabase
- PostgreSQL
- Claude
- MCP
- Microsoft 365
- DocuSign
- Vercel
Where this is applied
Page copy drafted with AI assistance. Skofi is accountable for it. Editorial policy
Have something shaped like this?
If any part of the above resembles a problem you are living with, a short call is the fastest way to find out whether it is the same problem underneath.
What happens on the call
- 15 minutes, no deck
- We map the process on the call
- A written scope, or an honest no