Bespoke CRM
A bespoke CRM for the staff who introduce, onboard and look after our retailer panel. Built on a fork of the open-source Twenty platform, deployed on AWS in eu-west-2, and wired into the same Salesforce instance the rest of the business runs on. The brief was simple: the BDMs and Sales Support team should never need to log into Salesforce to do their job.
A click-through of how a retailer moves from prospect to live. Two kanban boards, the record detail, the auto-handoff, the prescriptive task gates, the Salesforce write-through, and the portfolio MI a BDM sees when they sign in. Synthetic data throughout.
Why this exists
Until recently, the way our BDMs and Sales Support team tracked retailer onboarding was a patchwork of spreadsheets, Slack DMs and email threads. The work was visible to the people doing it, less so to anyone else, and the handoff between commercial and operations was a phone call. The source of truth for a live retailer sat in Salesforce, but Salesforce is configured for the back office (the people who set up lender waterfalls, manage commission, run regulatory returns) and not for the people on the front line bringing retailers in.
The brief I gave myself was to build the CRM the front-line team would actually choose to use, leave Salesforce as the back-office system of record, and make the two stay in sync without anyone needing to think about it. A small team, a strict budget, and an FCA-regulated context where we cannot afford to lose data or get audit trails wrong.
I built this from scratch. The codebase is mine, the architectural decisions are mine, the AWS infrastructure is mine, the Salesforce integration is mine, the prescriptive flow is mine. AI agents handled the bulk of the typing under my direction.
Who uses it
BDMs (Business Development Managers). A mix of self-employed and PAYE relationship managers covering different regions of the UK. They bring new retailers into the panel, look after the ones they have already brought in, and need to see the lender pipeline state for each retailer without opening Salesforce. Most of their day is in front of the kanban board labelled Prospecting.
Sales Support. The operations team that takes a converted prospect and walks it through compliance, contract, lender approval and technical setup. They live in front of the Onboarding kanban. Their work is the most prescriptive: every stage has a defined set of tasks that need to be completed, and some of those tasks are hard gates.
Compliance and HR. Compliance reviews retailer applications at the FCA checkpoint stage. HR uses the CRM to hold staff records, including personal details, contracts, salary and holiday allowances. The Staff section is locked down to a dedicated role; no other user sees it.
Leadership. Commercial Director, MD, and the Head of Sales need aggregate views: pipeline value, BDM portfolio performance, conversion rates, and the lender waterfall health of each retailer. The CRM surfaces these as dashboards on the same data Sales Support is editing live.
What it does
Two pipelines on one record. Prospecting (Prospect, Engaged, On Hold, Dead, Converted) belongs to the BDMs. Onboarding (File Collection, SMT Sign Off, Sent to Lender, Lender Approved, Tech Setup, Live, Dead) belongs to Sales Support. Both kanbans are views of the same Retailer object. When a BDM marks a retailer Converted in the prospecting kanban, a workflow pre-fills the onboarding stage to File Collection, and the same record appears for Sales Support in the next board to pick up.
Prescriptive tasks per stage. Every stage has a templated set of tasks created automatically when a record enters the stage. Some are hard gates (advancement is blocked until they are done), some are advisory. The KYC, FCA verification, director check and financial fitness tasks at SMT Sign Off are hard gates; the courtesy call task at Engaged is advisory. The list is configured per stage in a taskTemplate object, not in code, so the operations team can tune it.
A retailer record that mirrors what matters from Salesforce. Salesforce holds 140 custom fields on Account today, accumulated over years of business growth. The CRM mirrors a curated subset of those fields onto the Retailer object, organised into Identity, Compliance, Commercial, Lender Waterfall and MI groups, and only the fields that the BDMs and Sales Support team actually need to see or edit. The rest stay in Salesforce.
Write-through to Salesforce on stage transition. When a retailer reaches Tech Setup, the CRM upserts the Account and primary Contact into Salesforce via a Composite API call, idempotent on a UUID external ID. The Salesforce-side audit field Source_of_Update__c gets set to Twenty_CRM_Push so the Salesforce team can distinguish CRM-originated writes from manual or other integration writes.
Activity logging. Calls, emails, meetings and notes against a retailer get logged in the CRM and replicated to Salesforce as standard Tasks against the Account. The BDM never opens Salesforce to log a call; the Salesforce team never wonders where a meeting note came from.
MI enrichment from the existing AWS data pipeline. A scheduled Lambda reads Application and Application_Decision data from Salesforce every 30 minutes, computes per-retailer KPIs (active applications, conversion this month, lender approval rate, current pipeline volume) and writes them back to read-only fields on the Retailer record. A BDM signing into the CRM sees their portfolio with live MI without ever having to think about a query.
Confidential staff records. Everything you would expect on an HR record (personal details, employment history, contract type, salary, holiday allowance and used days, an emergency contact) lives in a Staff custom object, visible only to the HR Admin role. Standard members of the workspace cannot see that the object exists.
Architecture
Twenty (open-source CRM, AGPL-3.0), forked, hosted on AWS. Twenty runs as Docker containers on a single EC2 instance in the dev account, with Postgres on RDS and S3 for file attachments. An Application Load Balancer fronts it, Secrets Manager holds the database password and the workspace app secret, SSM Session Manager replaces SSH. The whole production-shaped stack costs around £77 a month while running and tears down in about a minute.
We do not modify Twenty's upstream source. All Shermin-specific configuration (renaming Companies to Retailers, adding the Staff custom object, defining the two pipeline stage selects, configuring the kanban views, building the conversion workflow shell, setting the role permissions, uploading the brand logo) is applied via Twenty's metadata API by an idempotent shell script. Re-running it on a fresh workspace rebuilds everything in about a minute. This is the only way to avoid drowning in upstream merge conflicts when Twenty ships a release.
One file-size patch is unavoidable. Twenty hardcodes a 10 MB upload limit and exposes no environment variable to override it. We carry a tiny Dockerfile.shermin that sed-patches the compiled constant from 10 MB to 100 MB, with a build-time assertion that the substitution actually landed. The cap matters because real FCA evidence (signed contracts, financial statements, KYC packs) frequently sits between 15 and 60 MB.
We share the existing Salesforce JWT auth helper. Every Shermin AWS service that talks to Salesforce already shares one Lambda that holds the JWT private key, signs assertions, and returns access tokens. The CRM joins as the fifth caller. We do not load the JWT secret directly, do not run our own Connected App, and do not duplicate the rotation playbook. Distinguishing CRM-originated writes from other writers in the audit log is done at the data layer with Source_of_Update__c, not at the auth layer.
Two new Lambdas, one new Terraform module, one new S3 state file. The CRM owns a sf-push Lambda (handles the upserts and activity-push from the CRM into Salesforce) and an mi-enrich Lambda (scheduled, reads Application pipeline data from Salesforce and writes KPI fields back into the CRM). Both are Python 3.13 to match the rest of the org. Terraform state is independent from the EC2 / RDS module so applies cannot risk-couple infrastructure with integration.
FCA-aligned S3 retention from day one. File attachments land in an S3 bucket with versioning, blocked public access, server-side encryption, and a lifecycle that transitions current versions to Glacier Instant Retrieval at 90 days and to Deep Archive at 7 years. Nothing is ever permanently deleted; CONC requires 6 years of retention for credit records and 7 gives a safety margin.
Notable engineering decisions
No new Salesforce Connected App. The cleanest separation would have been a dedicated CRM integration user with a dedicated Connected App. We considered it and declined, because the marginal benefit is small (cleaner API limit attribution) and the cost is large (more admin work for the Salesforce team, more rotation surface, more drift risk). Reusing the existing shared auth Lambda preserves the operational pattern everyone in the team already understands.
We deferred the visual brand override. Twenty's UI consumes brand colours through a JavaScript theme provider, not CSS variables. CSS injection works for some surfaces but the buttons and focus rings read theme values from the JS bundle at runtime. Patching that properly needs a front-end source build (clone Twenty, edit MainColorsLight.ts, rebuild). I chose to ship logo plus workspace name now and queue the front-end build as a separate piece of work for when external visibility actually matters.
We do not bidirectionally sync editable fields. The CRM owns its fields; the back office owns everything else in Salesforce. The integration is one way for writes (CRM to Salesforce on stage transition, CRM to Salesforce on activity log), one way for reads (Salesforce to CRM for MI enrichment), and the two domains do not overlap. Two-way sync looks elegant in a slide deck and produces conflict-resolution bugs in production.
Workflow content cannot be created via Twenty's API key. Twenty's WorkflowVersionStep resolver is gated by user-session auth. The setup script creates the workflow shell as a draft and a human completes the trigger and steps in the UI, a 3-minute click. We could have impersonated a session token in the script. We chose not to, because Twenty's gating exists for a reason and we are not going to argue with it.
Idempotent Salesforce upserts via a UUID external ID. Every Retailer record carries an external identifier generated client-side. The Salesforce write is a Composite API PATCH against Account/CRM_External_Id__c/<uuid>. Re-running the same write twice produces the same state. Webhook retries on failure cannot create duplicates.
Status
Live in dev. The data model is in place, both kanbans are configured, the Staff object is locked down, the logo is on, the conversion workflow shell exists. The Salesforce push Lambda has been built and verified end to end against the sandbox; describes Account and Contact, returning the 140 custom fields and 8 record types we knew were there.
Next, we map which subset of those 140 Account fields actually need to mirror to the CRM and at which stage, build the prescriptive task templates per stage, wire up the BDM portfolio MI dashboards, and graduate the whole stack from dev sandbox to a real domain with a real ACM certificate.
The architecture is deliberately small. One EC2, one Postgres, two Lambdas, one S3 bucket. It is the kind of thing a single engineer can hold in their head, and that is the most important property a system like this can have.