Green Compare CRM
An internal CRM for a UK business-energy and payments broker — from quote to contract to commission.
- Role
- Project initiator, then full-stack feature developer
- Team
- 4 engineers — XEOO team
- Timeline
- Jun 2026 – Ongoing
- Organisation
- Green Compare Ltd (XEOO team)
- Code
- Proprietary (employer)
- Reading time
- 5 min
A FastAPI modular monolith with a React 19 front end, where Postgres row-level security backs every permission check. I started the repository, then shipped features across finance, telesales, renewals, dashboards and access control.
Overview
Sellers and telesales reps source businesses, admins price them across suppliers and run credit checks, signed services become contracts tracked to renewal, and finance reconciles supplier commission and rep payouts. The CRM replaces the spreadsheets that used to hold all of this.
The problem
- A sales pipeline with many hand-offs: seller → admin pricing → credit check → signature → contract → renewal.
- Money flows (commission, clawbacks, partial payouts) that must be exact and auditable.
- Strict data boundaries: a rep must see only what their role and team allow — even if an API bug slips through.
Context & constraints
I created the repository with an AI-assisted first version and worked on its frontend before the team moved it to the company organisation, added CI/CD and grew it into a production system. The platform conventions — CI, deployment, security tooling and the row-level security sweep — were built by colleagues; I worked within them.
Approach
My contributions, across 38 merged pull requests:
- Finance — rebuilt the commissions area (overview, rep payouts, statements), clawback netting and partial payouts with a partly paid status.
- Telesales call floor — call-floor statuses and allocation, an append-only record of every dial, and two-way sync between list rows and the callbacks diary.
- Clients & contracts — filters by seller, service and date, a card view with analytics panels for reps, and book statistics computed on the server from one shared filter definition.
- Renewals — a re-signed meter stops being a renewal, derived at query time with no stored status that could drift.
- Dashboards — every role board rebuilt on a 12-column grid with a shared widget kit, plus a performance board ranking sellers and telesales over five time windows with drill-down pages. In October the seller, telesales and system-admin dashboards were rebuilt again on server-side reports — earnings, win rate, a “needs you” queue, a call funnel against a daily target, revenue mix and work queues — with every figure audited against the page it links to.
- Pipeline — a kanban board whose stage totals are counted on the server, role-aware filters, and a notification panel with deep links.
- Tasks & clients — a board and agenda view for tasks with real completion dates, accessible client rows, and a Files tab per client with an in-app PDF and image viewer, multi-file upload and permission-checked edits.
- Access control — renamed a role across backend, frontend, identity provider and database, including a migration that rewrote the live row-level security policies.
- Quality — a responsive end-to-end sweep: every route × role × 8 viewports × 2 orientations.
Key capabilities
- Quote round-trip from seller request to signed contract
- Per-product contracts with a renewals board
- Telesales call floor with callbacks diary
- Commission ledger with clawbacks and partial payouts
- Role dashboards and a performance board
- Pipeline kanban with server-counted stage totals
- Client files with an in-app viewer
- Document extraction with PII redaction before any model sees the text
Architecture
Interface
FastAPI modular monolith
Async work
Data
Two layers of authorisation: an application capability matrix decides scope, and Postgres row-level security enforces it underneath. Select a component to trace its connections.
Key decisions
ADR-01Derive renewals at query time
- Context
- A stored "renewal" flag goes stale the moment a customer re-signs, and stale flags send reps chasing deals that are already closed.
- Decision
- Compute renewal state in the query from contract dates and successor contracts; the incumbent closes as locked-in.
- Trade-offs
- A slightly heavier query, no state to keep in sync — and 14 tests pin the behaviour.
ADR-02One server-side filter definition for statistics
- Context
- Book totals were summed in the browser from paginated data, so numbers changed with page size.
- Decision
- Move statistics to the server, built from the same filter clause the list uses.
- Trade-offs
- An extra endpoint; totals now always match the visible list (10 tests).
ADR-03Rename a role all the way down
- Context
- A role name no longer matched the job. Renaming only the UI label would leave the database, policies and identity provider out of step.
- Decision
- Rename across code, identity provider and database, with a migration that regenerates the live row-level security policies from the catalogue.
- Trade-offs
- A riskier migration, rehearsed against real Postgres in CI, instead of permanent naming debt.
Challenges
Responsive layouts across seven roles
Problem. Every role sees a different set of pages, and layout bugs hid in combinations nobody tested by hand.
Solution. An end-to-end sweep renders every route for every role at 8 viewports in both orientations, failing on overflow.
Dashboard funnels reading the wrong keys
Problem. An admin funnel silently showed zeros because it read stage keys that no longer existed.
Solution. Rebuilding the dashboards on a shared widget kit with typed data sources surfaced and fixed the mismatch.
Quality & delivery
Testing
The project-wide suite holds 858 test functions as of late October 2026 (pytest with real Postgres via Testcontainers, property-based tests with Hypothesis) plus Playwright and axe-core accessibility sweeps. My pull requests added backend tests for performance boards, renewals, book statistics, role dashboards and client files.
Deployment
Six CI gates — secret scanning, backend tests, migration checks against real Postgres, frontend build, security audits and end-to-end accessibility — must pass before merge. Merging to main deploys after a fail-closed migration preflight and a database backup (pipeline built by colleagues).
Outcome
- Merged pull requests
- 38
- My contributions, Jun–Oct 2026
- Domain modules
- 18
- One FastAPI modular monolith
- Roles × capabilities
- 7 × 17
- Backed by row-level security
- Test functions project-wide
- 858
- Team total, late October 2026
The CRM runs in production for the company's sales, telesales, admin and finance teams. My finance, call-floor, renewals, pipeline, files and dashboard work is part of the daily workflow.
Lessons
- Put the hard security boundary in the database; application checks then become a convenience, not the last line of defence.
- State you can derive is state you never have to migrate.
Building something similar?
Tell me what you’re working on — I reply to every serious message.
Related work
HR Inbox Agent
A Python agent that reads an HR mailbox through Microsoft Graph, asks a language model for a structured classification and draft reply, and lets deterministic code decide whether to send, save a draft or escalate to Slack.
- Python
- SQLite
- Docker & Compose
- LLM APIs
- Agent guardrails & evals
- AI gateways
- +1
Business Energy Quote Funnel
A zero-dependency landing page and Node server for business-energy quotes, with progressive enhancement, ad attribution, spam protection and an offline postcode-to-street index built from open data.
- JavaScript
- Python
- HTML & CSS
- Node.js