Two distinct problems share this document. The first is operational: should a company migrate off Salesforce, and if so, to what? The second is architectural: if an engineering team is building a custom CRM to replace Salesforce, how do they validate API compatibility before flipping traffic? Both questions have concrete, practitioner-grade answers in 2026. Neither is a solved problem — both require real engineering work and budget honesty.
Part 1 — Salesforce Migration Alternatives
1.1 Why Companies Migrate Off Salesforce
Salesforce dominates enterprise CRM with ~23% global market share (IDC, 2025), but churn is rising in the 10–250 seat band. The proximate causes cluster around three vectors:
Cost escalation. Sales Cloud Enterprise lists at $165/user/month as of 2026, but true total cost of ownership is materially higher once you add:
| Cost Component | Typical Range |
|---|---|
| Sales Cloud Enterprise license | $165/user/month |
| Marketing Cloud Add-on | $400–$3,500/month flat |
| CPQ (Salesforce Revenue Cloud) | $75/user/month add-on |
| AppExchange ISV apps (avg 3–5) | $20–$80/user/month |
| Admin salary (1 FTE per 50 seats) | ~$110,000–$145,000/year |
| Salesforce storage overages | $5–$10/GB/month above 10GB |
| AI/Einstein consumption fees | variable, $75–$150/user/month |
| Realistic loaded cost | $300–$500/user/month |
At 50 users, that is $180,000–$300,000/year just for the platform. At 200 users, a Fortune 500 subsidiary can be spending $720,000–$1.2M/year before touching services or integrations.
Complexity accumulation. Salesforce orgs age poorly. Every process builder flow, legacy workflow rule, custom object relationship, and Apex trigger added over 5–10 years compounds maintenance burden. Salesforce’s own migration from Process Builder to Flow has created a large installed base of orgs running deprecated automation that nobody wants to touch.
Vendor lock-in. Proprietary SOQL, Apex (proprietary OOP language), VisualForce and Aura (legacy front-end frameworks), Lightning Web Components, and the Metadata API all create moat. Data portability is real — Salesforce’s Bulk API 2.0 exports everything — but the surrounding logic, automation, and integrations do not travel.
1.2 Realistic Cheaper Alternatives
Tier 1 — Commercial SaaS (Significant Savings, Low Migration Risk)
HubSpot CRM
- Free tier: unlimited contacts and deals, core pipeline views, email integration. Functional for teams under 10 not needing sequences or reporting.
- Sales Hub Starter: $20/user/month (billed annually). Adds email sequences, meeting scheduling, simple pipelines.
- Sales Hub Professional: $100/user/month. Adds custom reporting, forecasting, playbooks, deal automation. This is the Salesforce Professional analog.
- Sales Hub Enterprise: $150/user/month. Adds custom objects, predictive forecasting, call transcription.
- Migration advantage: HubSpot maintains a native Salesforce Data Sync integration that can run bidirectionally during transition. Standard objects (Contacts, Companies, Deals, Activities) map cleanly. Custom objects require HubSpot Enterprise.
Pipedrive
- Essential: $14/user/month. Core pipeline, activities, email sync.
- Advanced: $29/user/month. Automation, email sequences, scheduler.
- Professional: $59/user/month. Revenue forecasting, custom reporting, multiple pipelines.
- Power: $69/user/month. Collaboration tools, project management layer.
- Enterprise: $99/user/month. Full audit logs, SSO, enhanced support.
- Best fit: Pure sales teams with a linear deal process. Weak on marketing automation and customer success workflows. Excels at pipeline visualization and activity tracking.
Zoho CRM
- Standard: $14/user/month. Basic CRM, 10 custom fields, email integration.
- Professional: $23/user/month. SalesSignals, web forms, scoring rules, inventory.
- Enterprise: $40/user/month. Zia AI, multi-user portals, advanced analytics.
- Ultimate: $52/user/month. Full feature set, dedicated BI analytics.
- Migration advantage: Zoho offers a native Salesforce Migration Wizard (free, self-serve) that migrates Accounts, Contacts, Leads, Opportunities, Cases, and custom modules. Field mapping is automated with manual review required for custom picklists and formulas. Zoho’s ecosystem (Zoho Books, Campaigns, Desk, Projects) rivals Salesforce’s breadth at a fraction of the cost.
Tier 2 — Open Source / Self-Hosted (Maximum Savings, Real Ops Overhead)
Twenty CRM (twenty.dev / twentyhq/twenty on GitHub)
Twenty is the highest-momentum open-source CRM in 2026. As of June 2026:
- 44,000+ GitHub stars, 6,000+ forks, 300+ contributors
- Stack: TypeScript monorepo, NestJS back-end, React front-end, PostgreSQL, Redis
- APIs: REST + GraphQL (auto-generated from metadata definitions)
- Licensing: AGPL-3.0 (self-hosted free forever, no user caps, no feature gates)
- Cloud managed (app.twenty.com): $9/user/month
- Deployment: Docker Compose (single-node), Kubernetes Helm chart (production)
Twenty’s architecture is fundamentally different from legacy CRMs: objects (People, Companies, Deals) are defined as metadata in the database, not hard-coded. This means custom objects are a first-class primitive, not an afterthought. The GraphQL API surface auto-generates to include any custom object instantly.
Capabilities in 2026:
- Custom objects with arbitrary field types (text, number, relation, select, date, file, link)
- Kanban and table views with filter/sort/group
- Email and calendar sync (Gmail, Outlook via OAuth)
- Timeline view of all activities per record
- Workflow automation (trigger → action chains, beta as of early 2026)
- REST and GraphQL APIs fully documented at /api
- Webhook support for outbound events
Gaps vs. Salesforce (as of mid-2026):
- No native CPQ / Revenue Cloud equivalent
- No AppExchange ecosystem (third-party integrations are DIY or via Zapier/n8n)
- Workflow automation is early-stage (Salesforce Flow equivalent not yet complete)
- No built-in telephony, predictive scoring, or Einstein-class AI
- Territory management absent
- Reporting is functional but not Salesforce Analytics depth
SuiteCRM
- Open-source fork of SugarCRM Community Edition (SalesAgility, UK)
- ~5M claimed users across enterprise, government, NGOs
- Stack: PHP 8, MySQL/MariaDB/PostgreSQL, Smarty templates
- Closest feature parity to Salesforce of any open-source option
- Modules: Accounts, Contacts, Leads, Opportunities, Cases, Campaigns, Contracts, Products, Quotes, Reports, Workflow (AOW), Email Marketing
- Critical limitation: UI and codebase are dated (PHP MVC, no modern front-end). New contributor velocity is slower than Twenty. Not API-first.
- Best for: organizations with Salesforce-parity requirements, in-house PHP/LAMP expertise, no need for a modern developer experience.
Erxes
- Open-source, TypeScript/Node, React, MongoDB
- Focuses on marketing + sales + support (XOS architecture — “Experience Operating System”)
- Plugin-based architecture; each module (inbox, CRM, marketing) is a separate plugin
- Better for companies that need a unified customer engagement platform, not just a sales pipeline
- Self-hosted free, cloud plans from $149/month flat
1.3 Decision Framework: Who Should Migrate vs. Stay
┌──────────────────────────────────────────────────────────────────┐
│ MIGRATION DECISION MATRIX │
├─────────────────────┬────────────────────────────────────────────┤
│ SIGNAL │ RECOMMENDATION │
├─────────────────────┼────────────────────────────────────────────┤
│ < 25 seats │ Migrate. Salesforce ROI almost never │
│ │ justifies cost below this threshold. │
│ │ Target: HubSpot Starter or Twenty CRM. │
├─────────────────────┼────────────────────────────────────────────┤
│ 25–100 seats, │ Migrate with planning. 6–12 month │
│ few custom objects │ timeline realistic. HubSpot Pro or │
│ │ Zoho Enterprise are lowest-risk landings. │
├─────────────────────┼────────────────────────────────────────────┤
│ 25–100 seats, │ Evaluate carefully. Custom object │
│ heavy customization │ migration is the primary risk. Requires │
│ │ dedicated data engineering work. │
├─────────────────────┼────────────────────────────────────────────┤
│ >100 seats, │ Stay or rebuild internally. Integration │
│ deep integrations │ dependency graph is the real lock-in, │
│ (ERP, CPQ, etc.) │ not the license. ROI on migration is │
│ │ negative unless contract is expiring. │
├─────────────────────┼────────────────────────────────────────────┤
│ Engineering-led │ Consider Twenty CRM (self-hosted) or │
│ company, Postgres │ build on Twenty's API layer. Total cost │
│ fluent ops team │ including hosting: $5–15K/year for 50 │
│ │ users vs. $100K+ for Salesforce. │
├─────────────────────┼────────────────────────────────────────────┤
│ Revenue Cloud / │ Do not migrate. No credible alternative │
│ CPQ in production │ replicates Salesforce CPQ depth. Cost of │
│ │ rebuilding quoting logic exceeds savings. │
├─────────────────────┼────────────────────────────────────────────┤
│ Salesforce-native │ Do not migrate. Apex, custom LWC, and │
│ code in production │ Metadata API deployments are not portable. │
│ (Apex / LWC) │ Rewrite cost is 2–3x migration savings. │
└─────────────────────┴────────────────────────────────────────────┘
1.4 Migration Complexity and Real Costs
What Moves Cleanly
Standard object data (Accounts, Contacts, Leads, Opportunities, Activities, Tasks, Events) exports via Salesforce’s native Weekly Data Export or Bulk API 2.0 as CSV or JSON. Field types are well-documented. Most target CRMs provide import wizards for these.
What Requires Engineering Work
| Migration Component | Effort | Notes |
|---|---|---|
| Standard object data | Low | CSV export → import wizard |
| Custom object data | Medium | Must map fields manually; picklist values need reconciliation |
| Custom picklist dependencies | Medium | Controlling field → dependent field logic must be rebuilt |
| Workflow Rules (legacy) | High | No export format; must be read from Salesforce UI and rebuilt |
| Process Builder flows | High | Deprecated by Salesforce; must be rewritten in target platform |
| Salesforce Flow | Very High | Visual flows export as XML metadata but are proprietary; no standard |
| Apex triggers | Very High | Proprietary language; logic must be rewritten in target language |
| Email templates | Medium | HTML-based; portable with reformatting |
| Reports and dashboards | High | SOQL-backed reports do not port; must be rebuilt |
| AppExchange ISV integrations | Very High | Each ISV must be replaced with a native integration or rebuilt |
| ERP / billing integrations | Very High | Often the actual migration-killer; re-architecture required |
| User permissions / profiles / roles | Medium | Role hierarchy logic must be rebuilt; RBAC models differ |
| Document generation | High | Salesforce Doc Gen is proprietary; tools like Conga require alternatives |
Realistic Timeline
Month 1–2: Audit and inventory
- Map all objects, fields, automations, integrations
- Score each component (moves clean / needs rebuild / blocks migration)
- Select target platform
- Sign contract with clear data portability provisions
Month 2–3: Data extraction and cleansing
- Bulk API 2.0 export of all objects
- Deduplication (expect 10–30% duplicate rate in aging orgs)
- Field normalization (phone formats, country codes, etc.)
Month 3–6: Target platform build
- Custom object definitions
- Workflow/automation rebuild
- Integration reconnection (one by one)
- Permission model configuration
Month 6–9: Parallel run
- Both systems live; new records enter target CRM
- Salesforce used as read-only reference
- Issue triage and fix cycle
Month 9–12: Cutover and decommission
- Final delta sync from Salesforce
- Training and change management
- Salesforce contract non-renewal
Month 12–18: Stabilization
- Long-tail integrations reconnected
- Reporting rebuilt to equivalent coverage
- Salesforce license termination
Hidden Cost Inventory
The decision to migrate often looks like a no-brainer on a spreadsheet until someone does a proper audit. Budget for:
- Data cleansing: $5,000–$60,000 depending on volume and quality. A 50,000-record migration with 15,000 duplicates and no naming standards can take 2–3x longer than a clean export.
- Integration re-architecture: $10,000–$100,000+. Every Salesforce integration (Marketo, Netsuite, Zendesk, Docusign, Slack) must be individually re-plumbed. Middleware costs (Zapier, Make, Tray.io, or custom Node/Python services) add up.
- Workflow rebuilds: $20,000–$80,000 for complex orgs. Rule of thumb: 1 week of developer time per 10 non-trivial automation rules.
- Retraining: $500–$5,000 per user (Salesforce’s own implementation cost guides cite this range). Salesforce-trained users have muscle memory that does not transfer. Budget for role-specific training, not generic onboarding.
- Lost productivity during transition: Typically 30–60 days of below-normal output from the sales team during cutover. Hard to quantify, real to measure.
- Consultant fees: If using a migration partner, add $15,000–$80,000 for a typical SMB migration.
Rule of thumb: Total migration cost (one-time) equals 6–18 months of the Salesforce license being replaced. The break-even point for a 50-seat org moving from Salesforce Enterprise to HubSpot Pro is typically month 18–24.
Part 2 — Salesforce API Proxy for Custom CRM
2.1 The Problem Statement
An engineering team building a custom CRM to replace Salesforce faces a specific validation challenge: existing integrations — ETL pipelines, marketing automation tools, ERP connectors, custom scripts — all speak Salesforce’s REST API. Before decommissioning Salesforce, the team needs to prove their custom CRM responds to Salesforce API calls correctly.
The cleanest approach is a record-then-validate loop:
- Place a transparent proxy in front of Salesforce in production
- Record all real API traffic (request/response pairs)
- Point that recorded traffic at the custom CRM
- Compare responses
This produces a ground-truth compatibility test corpus derived from actual production behavior, not synthetic tests.
2.2 Salesforce REST API Surface Map
A proxy that claims 100% compatibility must cover the entire API surface. Salesforce exposes seven distinct API families:
REST API (Primary — must implement)
| Endpoint Pattern | Purpose |
|---|---|
GET /services/data/ |
API version list |
GET /services/data/v{n}/ |
Resources available at version |
GET /services/data/v{n}/limits |
Governor limit status |
GET /services/data/v{n}/sobjects/ |
Object catalog |
GET /services/data/v{n}/sobjects/{Object}/ |
Object metadata |
GET /services/data/v{n}/sobjects/{Object}/describe |
Full field/relationship schema |
POST /services/data/v{n}/sobjects/{Object}/ |
Create record |
GET /services/data/v{n}/sobjects/{Object}/{id} |
Read record |
PATCH /services/data/v{n}/sobjects/{Object}/{id} |
Update record |
DELETE /services/data/v{n}/sobjects/{Object}/{id} |
Delete record |
GET /services/data/v{n}/sobjects/{Object}/updated |
Changed records by time range |
GET /services/data/v{n}/sobjects/{Object}/deleted |
Deleted records by time range |
GET /services/data/v{n}/query/?q={SOQL} |
SOQL query |
GET /services/data/v{n}/query/{queryLocator} |
SOQL pagination (cursor) |
GET /services/data/v{n}/queryAll/?q={SOQL} |
SOQL including deleted records |
GET /services/data/v{n}/search/?q={SOSL} |
SOSL full-text search |
PATCH /services/data/v{n}/sobjects/{Object}/{extIdField}/{extId} |
Upsert by external ID |
GET /services/data/v{n}/composite/sobjects |
Composite collections |
POST /services/data/v{n}/composite/sobjects |
Bulk create/update (up to 200) |
POST /services/data/v{n}/composite/ |
Composite request (up to 25 subrequests) |
POST /services/data/v{n}/composite/batch |
Batch request (independent subrequests) |
POST /services/data/v{n}/composite/tree/{Object} |
Tree insertion (parent + children) |
Bulk API 2.0 (Required for ETL compatibility)
| Endpoint | Purpose |
|---|---|
POST /services/data/v{n}/jobs/ingest |
Create ingest job |
PUT /services/data/v{n}/jobs/ingest/{jobId}/batches |
Upload CSV data |
PATCH /services/data/v{n}/jobs/ingest/{jobId} |
Close/abort job |
GET /services/data/v{n}/jobs/ingest/{jobId} |
Job status |
GET /services/data/v{n}/jobs/ingest/{jobId}/successfulResults |
Success records |
GET /services/data/v{n}/jobs/ingest/{jobId}/failedResults |
Failed records |
POST /services/data/v{n}/jobs/query |
Create query job |
GET /services/data/v{n}/jobs/query/{jobId}/results |
Download results CSV |
Authentication (OAuth 2.0 — mandatory)
| Flow | Use Case |
|---|---|
| Web Server Flow (Authorization Code) | User-facing integrations |
| JWT Bearer Token Flow | Server-to-server (no user interaction) |
| Username-Password Flow (deprecated) | Legacy scripts; still in production everywhere |
| Device Flow | IoT / CLI tools |
| Refresh Token Flow | Token lifecycle management |
The token endpoint is https://{instance}/services/oauth2/token. The userinfo endpoint is https://{instance}/services/oauth2/userinfo. A proxy must implement all of these.
Streaming / Events (Required for change-data-capture consumers)
Salesforce’s event system has two generations:
- Streaming API (legacy): CometD/Bayeux protocol over long-polling HTTP. Endpoint:
https://{instance}/cometd/{version}/. Topics:/topic/{PushTopic},/event/{PlatformEvent__e},/data/{Object}ChangeEvent. Requires CometD handshake → subscribe → poll cycle. - Pub/Sub API (current): gRPC + HTTP/2, Apache Avro binary serialization, pull-based flow control. Schema registry at
/api/platform/v1/schema/{schemaId}. Recommended for new integrations.
A custom CRM targeting CDC consumers must implement either CometD long-poll server (Streaming API compatibility) or a gRPC endpoint conforming to the Pub/Sub API proto schema.
Metadata API (Required if any Salesforce tooling — Workbench, VS Code extension — is used)
SOAP-based. Exposes: retrieve, deploy, listMetadata, describeMetadata, checkRetrieveStatus, checkDeployStatus. Most migration tooling depends on this. Not required for runtime CRM compatibility but required for org-management compatibility.
Tooling API (Lower priority)
REST + SOQL-like interface for development tooling: code coverage, debug logs, test execution. Required only if Salesforce developer tooling is pointed at the custom CRM.
2.3 Proxy Architectures
Three architectural patterns cover the main use cases.
Pattern A — Transparent Pass-Through Proxy with Traffic Recording
┌──────────────────────────────────────────────────────────────────┐
│ PATTERN A: TRANSPARENT PROXY │
│ │
│ Integration Recording Proxy Salesforce │
│ (ETL, App, ──► ┌────────────────┐ ──► ┌─────────────┐ │
│ Script) │ nginx / Envoy │ │ Production │ │
│ │ │ │ Org │ │
│ │ 1. Intercept │ ◄── └─────────────┘ │
│ │ 2. Log req/res │ │
│ │ 3. Forward as- │ │
│ │ is to SF │ │
│ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Log Store │ │
│ │ (Hoverfly / │ │
│ │ WireMock / │ │
│ │ raw JSONL) │ │
│ └────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Best for: capturing production traffic corpus without modifying integrations. Zero-risk to production.
nginx implementation sketch:
# /etc/nginx/conf.d/sf-proxy.conf
server {
listen 443 ssl;
server_name sf-proxy.internal;
ssl_certificate /etc/ssl/sf-proxy.crt;
ssl_certificate_key /etc/ssl/sf-proxy.key;
# Log full request/response bodies for replay corpus
lua_need_request_body on;
body_filter_by_lua_block {
-- Emit req+res pair to log collector
local req_body = ngx.req.get_body_data() or ""
local resp_body = ngx.arg[1] or ""
ngx.log(ngx.NOTICE,
"SFPROXY ",
ngx.var.request_method, " ",
ngx.var.request_uri, " ",
ngx.status, " ",
ngx.encode_base64(req_body), " ",
ngx.encode_base64(resp_body))
}
location / {
proxy_pass https://your-org.my.salesforce.com;
proxy_set_header Host your-org.my.salesforce.com;
proxy_ssl_server_name on;
# Credential injection: replace client tokens with proxy service account
# (only if integrations use shared credentials)
# proxy_set_header Authorization "Bearer ${SF_SERVICE_TOKEN}";
}
}
For body logging without OpenResty/Lua, use log_format with $request_body and $upstream_response_body (requires ngx_http_echo_module for response body capture).
Pattern B — Envoy with Credential Injection
Envoy Gateway (v1.3+, 2025) natively supports HTTPCredentialInjectionFilter, which injects an Authorization header into proxied requests. This is the right tool when:
- Integrations use stale or per-user tokens
- You want a single service-account credential at the proxy layer
- You need mTLS between proxy and Salesforce
# EnvoyGateway HTTPRoute with credential injection
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: HTTPRouteFilter
metadata:
name: sf-credential-injector
spec:
type: RequestHeaderModifier
requestHeaderModifier:
set:
- name: Authorization
value: "Bearer ${SF_PROXY_TOKEN}"
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: sf-proxy-route
spec:
rules:
- backendRefs:
- name: salesforce-backend
port: 443
filters:
- type: ExtensionRef
extensionRef:
group: gateway.envoyproxy.io
kind: HTTPRouteFilter
name: sf-credential-injector
Pattern C — Purpose-Built API Gateway with Request Transformation
When the custom CRM uses a different URL scheme or slightly different field names, a transformation layer sits between the integration and the custom CRM. This is the “shim” pattern used during phased migration.
┌──────────────────────────────────────────────────────────────────┐
│ PATTERN C: TRANSFORMATION GATEWAY │
│ │
│ Integration API Gateway / Shim Custom CRM │
│ (speaks SF ──► ┌────────────────────────┐ ──► ┌─────────────┐ │
│ REST API) │ │ │ (Postgres │ │
│ │ SF Request → │ │ backed, │ │
│ │ Normalize → │ ◄── │ REST/GQL) │ │
│ │ Route to CRM │ └─────────────┘ │
│ │ │ │
│ │ CRM Response → │ │
│ │ Transform to SF │ │
│ │ Response shape │ │
│ └────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
This pattern is the most engineering-intensive but enables a true drop-in replacement. The transformation layer must handle:
- URL rewriting:
/services/data/v60.0/sobjects/Account/→/api/objects/accounts/ - Field name mapping: Salesforce uses
PascalCasefield names with__csuffix for custom fields. Custom CRM may usesnake_case. - SOQL → SQL translation: The proxy must parse SOQL and emit equivalent queries against the custom CRM’s query endpoint.
- Response envelope normalization: Salesforce responses include
attributesobjects withtypeandurl; custom CRMs typically omit these. - Error code mapping: Salesforce error codes (
REQUIRED_FIELD_MISSING,DUPLICATE_VALUE, etc.) must map to equivalent responses.
2.4 Record/Replay Validation Workflow
This is the definitive approach for validating custom CRM compatibility before cutover.
Step 1 — Instrument Production Proxy
Deploy Pattern A proxy in front of production Salesforce. Let it run for 2–4 weeks to capture representative traffic across all integrations and usage patterns. Capture:
- Method, URL (full path + query string)
- Request headers (sanitize auth tokens in storage)
- Request body (JSON/CSV)
- Response status code
- Response headers
- Response body
- Latency
- Timestamp
Store as JSONL (one record per request/response pair). A medium-sized org generates 10,000–100,000 API calls/day.
Step 2 — Build Replay Corpus
Filter the raw log to a canonical test corpus:
- Remove health checks and metadata-only requests
- Deduplicate by (method, path-template, response-status) — keep examples of each unique pattern
- Anonymize PII in request/response bodies (replace real names, emails, SSNs with generated fixtures)
- Tag by integration source (ETL pipeline vs. marketing tool vs. custom app)
Target size: 500–5,000 request/response pairs covering all object types and operations.
Step 3 — Tool Options for Replay
Hoverfly (preferred for high-volume, Go-native):
# Import recorded traffic
hoverfly --import recorded-sf-traffic.json
# Switch to simulate mode
hoverctl mode simulate
# Run integrations against Hoverfly → validates they get SF-compatible responses
# Then re-run against custom CRM and diff responses
Hoverfly’s “diff” mode captures deviations between expected (recorded from SF) and actual (from custom CRM) response. This is the diff report used to drive implementation.
WireMock (preferred for Java shops):
# Start WireMock in proxy+record mode
java -jar wiremock-standalone.jar \
--proxy-all="https://your-org.my.salesforce.com" \
--record-mappings \
--port 8080
# Mappings are saved to /mappings/*.json and /__files/*.json
# Re-run integrations in stub mode against custom CRM
java -jar wiremock-standalone.jar --port 8080
WireMock supports request templating, which enables a single recorded mapping to match multiple record IDs (via regex/JsonPath matching) rather than requiring per-ID stubs.
Custom JSONL Replayer (for teams who want full control): A ~200-line Python or Node script that reads the JSONL corpus, replays each request against the target CRM, and diffs the response. This is the right choice when you need:
- Custom diffing rules (e.g., ignore
lastModifiedDatefields) - Integration with your test framework (pytest, Jest)
- CI/CD pipeline integration
# Skeleton: sf_replay.py
import json, requests, deepdiff
def replay_corpus(corpus_path: str, target_base_url: str, ignore_fields: list[str]):
results = {"pass": 0, "fail": 0, "diffs": []}
with open(corpus_path) as f:
for line in f:
rec = json.loads(line)
resp = requests.request(
method=rec["method"],
url=target_base_url + rec["path"],
params=rec.get("query_params"),
headers=sanitize_headers(rec["request_headers"]),
json=rec.get("request_body"),
timeout=10,
)
diff = deepdiff.DeepDiff(
rec["response_body"],
resp.json(),
exclude_paths=[f"root['{f}']" for f in ignore_fields],
)
if diff:
results["fail"] += 1
results["diffs"].append({"request": rec["path"], "diff": str(diff)})
else:
results["pass"] += 1
return results
Step 4 — Compatibility Scoring
Run the replay corpus against the custom CRM. Score by:
| Metric | Target |
|---|---|
| HTTP status code match rate | 100% |
| Response body structural match (all keys present) | 100% |
| Response body value match (non-volatile fields) | >98% |
| Bulk API job completion within 2x SF latency | >95% |
| OAuth token exchange round-trip | 100% |
| SOQL pagination (multi-page queries) | 100% |
Volatile fields (timestamps, generated IDs) should be excluded from value-match scoring.
2.5 Salesforce OpenAPI Spec: Status and Generation
Salesforce entered beta on OpenAPI 3.0 spec generation for the sObjects REST API in Spring '21 (API v51.0) and has iterated since. As of Summer '25 (API v64.0):
- Generation endpoint:
GET /services/data/v64.0/openapi/3.0 - Scope: Covers only the sObjects REST API (CRUD + describe endpoints). Does NOT cover: SOQL/SOSL endpoints, Bulk API 2.0, Streaming API, Composite/Batch, Pub/Sub API.
- Org-specific: The generated spec reflects the actual objects and fields in your org (including custom objects). It is not a generic Salesforce spec.
- Permissions required: Modify All Data OR Customize Application.
Practical use:
# Generate org-specific OpenAPI spec (requires valid access token)
curl -H "Authorization: Bearer $SF_TOKEN" \
"https://your-org.my.salesforce.com/services/data/v64.0/openapi/3.0" \
-o salesforce-org-openapi.json
# Validate your custom CRM against it using OpenAPI conformance tools
npx @schemathesis/schemathesis run \
salesforce-org-openapi.json \
--base-url http://localhost:8080 \
--auth-type bearer \
--auth $CUSTOM_CRM_TOKEN
Gap: No official OpenAPI spec exists for the SOQL query endpoint, Bulk API 2.0, Streaming API, or Pub/Sub API. These must be implemented from documentation, not a machine-readable spec.
2.6 SOQL → SQL Translation
SOQL is SQL-shaped but has Salesforce-specific extensions that require translation when building a compatible query layer.
| SOQL Feature | SQL Equivalent | Notes |
|---|---|---|
SELECT Id, Name FROM Account |
SELECT id, name FROM account |
Trivial; field name casing normalization needed |
WHERE CreatedDate > 2024-01-01T00:00:00Z |
WHERE created_date > '2024-01-01' |
Salesforce date literals (LAST_N_DAYS, THIS_YEAR, etc.) need special handling |
LIMIT 100 OFFSET 200 |
LIMIT 100 OFFSET 200 |
Direct; same semantics |
ORDER BY Name ASC NULLS LAST |
ORDER BY name ASC NULLS LAST |
PostgreSQL supports NULLS LAST natively |
SELECT Id, (SELECT Id FROM Contacts) FROM Account |
LEFT JOIN or subquery | Salesforce relationship queries (child → parent, parent → child) require join logic |
SELECT Account.Name FROM Contact |
SELECT a.name FROM contact c JOIN account a ON c.account_id = a.id |
Parent relationship traversal; dot notation maps to FK joins |
WHERE Name LIKE 'Acme%' |
WHERE name LIKE 'Acme%' |
Direct |
WHERE Id IN ('001...', '002...') |
WHERE id IN ('001...', '002...') |
Direct |
SELECT COUNT() FROM Account |
SELECT COUNT(*) FROM account |
COUNT() with no args is Salesforce-specific |
WHERE Name = null |
WHERE name IS NULL |
Salesforce uses = null, SQL uses IS NULL |
USING SCOPE mine |
WHERE owner_id = current_user_id() |
Scope filters require current user context injection |
WITH SECURITY_ENFORCED |
(RBAC check in middleware) | Enforces field/object FLS; no SQL equivalent, must be handled in app layer |
SOQL date literals are the most common pain point:
TODAY → CURRENT_DATE
YESTERDAY → CURRENT_DATE - INTERVAL '1 day'
LAST_7_DAYS → CURRENT_DATE - INTERVAL '7 days'
LAST_N_DAYS:30 → CURRENT_DATE - INTERVAL '30 days'
THIS_MONTH → DATE_TRUNC('month', CURRENT_DATE)
LAST_MONTH → DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '1 month'
THIS_QUARTER → (requires quarter arithmetic)
FISCAL_YEAR → (requires org-specific fiscal year config)
A production-grade SOQL parser exists: jsforce (JavaScript, open-source) parses SOQL into an AST. From the AST, generating PostgreSQL SQL is straightforward. Python option: simple-salesforce does not parse SOQL, but antlr4-soql (community) provides a grammar-based parser.
2.7 Practical Compatibility Checklist
Use this checklist before declaring a custom CRM “Salesforce API compatible” for a specific integration.
AUTHENTICATION
[ ] OAuth 2.0 Web Server Flow (authorization_code grant)
[ ] OAuth 2.0 JWT Bearer Flow
[ ] OAuth 2.0 Username-Password Flow (legacy support)
[ ] OAuth 2.0 Refresh Token grant
[ ] Token introspection endpoint
[ ] /services/oauth2/userinfo endpoint returns {sub, user_id, organization_id}
[ ] Instance URL in token response matches proxy base URL
SOBJECTS REST API
[ ] GET /services/data/ → returns [{version, url}] array
[ ] GET /services/data/v{n}/sobjects/ → returns {sobjects: [...]} with all objects
[ ] GET /services/data/v{n}/sobjects/{Object}/describe → full field metadata
[ ] POST /services/data/v{n}/sobjects/{Object}/ → returns {id, success, errors}
[ ] GET /services/data/v{n}/sobjects/{Object}/{id} → record with attributes object
[ ] PATCH /services/data/v{n}/sobjects/{Object}/{id} → 204 No Content on success
[ ] DELETE /services/data/v{n}/sobjects/{Object}/{id} → 204 No Content on success
[ ] PATCH upsert by external ID field
[ ] GET /updated?start=...&end=... → {ids, latestDateCovered}
[ ] GET /deleted?start=...&end=... → {deletedRecords, earliestDateAvailable, latestDateCovered}
QUERY
[ ] GET /services/data/v{n}/query/?q=... → {totalSize, done, records}
[ ] Pagination via nextRecordsUrl → GET /services/data/v{n}/query/{queryLocator}
[ ] GET /services/data/v{n}/queryAll/?q=... (includes deleted/archived)
[ ] SOQL date literals (TODAY, LAST_N_DAYS:n, THIS_MONTH, etc.)
[ ] SOQL relationship queries (child-to-parent dot notation)
[ ] SOQL subqueries (parent-to-child)
[ ] SOQL aggregate functions (COUNT, SUM, MAX, MIN, AVG)
[ ] SOQL GROUP BY and HAVING
[ ] SOQL ORDER BY NULLS FIRST/LAST
COMPOSITE / BATCH
[ ] POST /composite/ → up to 25 subrequests, referenceId chaining
[ ] POST /composite/batch → independent subrequests, allOrNone flag
[ ] POST /composite/sobjects → up to 200 records create/update
[ ] POST /composite/tree/{Object} → nested parent + children insert
BULK API 2.0
[ ] Create ingest job (insert, update, upsert, delete, hardDelete)
[ ] Upload CSV in batches
[ ] Close job → triggers async processing
[ ] Poll job status → (Open, UploadComplete, InProgress, JobComplete, Failed, Aborted)
[ ] Download successfulResults, failedResults, unprocessedRecords as CSV
[ ] Create query job → async SOQL
[ ] Download query results as CSV
ERROR RESPONSES
[ ] 400 with [{message, errorCode, fields}] array for validation errors
[ ] Standard Salesforce error codes (REQUIRED_FIELD_MISSING, DUPLICATE_VALUE,
FIELD_CUSTOM_VALIDATION_EXCEPTION, INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY)
[ ] 401 with {error: "INVALID_SESSION_ID", message: "..."} for auth failures
[ ] 404 with [{errorCode: "NOT_FOUND", message: "..."}]
STREAMING / EVENTS (if CDC consumers exist)
[ ] CometD handshake at /cometd/{version}/
[ ] PushTopic subscription (/topic/{name})
[ ] Platform Event subscription (/event/{Event__e})
[ ] Change Data Capture subscription (/data/{Object}ChangeEvent)
[ ] Pub/Sub API gRPC endpoint (if modern CDC consumers)
2.8 Tools Reference
| Tool | Role | Language | Notes |
|---|---|---|---|
| Hoverfly | Record/replay proxy | Go | Best for high-volume traffic capture; diff mode for comparison |
| WireMock | Record/replay + stub server | Java | Best for Java integration test suites; rich request matching |
| Prism (Stoplight) | OpenAPI mock + validation proxy | Node | Validates requests/responses against OpenAPI spec in real-time |
| Schemathesis | OpenAPI conformance testing | Python | Generates test cases from OpenAPI spec; fuzzes edge cases |
| jsforce | Salesforce API client + SOQL parser | Node/JS | Use for SOQL AST generation in a Node-based proxy |
| simple-salesforce | Salesforce API client | Python | Use for scripting replay corpus construction |
| nginx + OpenResty | Transparent proxy + Lua scripting | C/Lua | Body capture requires Lua; production-grade |
| Envoy Gateway | Credential injection + routing | Go | Native HTTPCredentialInjectionFilter in v1.3+ |
| Kong | API gateway with transformation plugins | Lua/Go | Request/response transform plugins; commercial plugins available |
| Caddy | Reverse proxy with simple config | Go | Simpler than nginx for basic pass-through; limited body logging |
| Twenty CRM | Custom CRM target | TypeScript | REST + GraphQL APIs; Postgres-backed; best open-source candidate for SF drop-in |
2.9 Architecture Recommendation for Custom CRM Drop-In
If the goal is a custom Postgres-backed CRM that accepts Salesforce REST API calls transparently, the recommended architecture is:
┌─────────────────────────────────────────────────────────────────┐
│ RECOMMENDED DROP-IN REPLACEMENT ARCHITECTURE │
│ │
│ Phase 1 (Record) Phase 2 (Build) Phase 3 (Cutover)│
│ │
│ All integrations Build custom CRM Flip DNS: │
│ │ with SF-compatible integrations now │
│ ▼ API layer. hit custom CRM │
│ Hoverfly proxy ──► Run Hoverfly replay directly. │
│ in front of SF corpus against CRM. │
│ (record mode) Fix diffs. Monitor replay │
│ │ corpus score │
│ ▼ Target: >99% pass continuously. │
│ JSONL corpus rate before cutover. │
│ (2-4 weeks of │
│ real traffic) │
└─────────────────────────────────────────────────────────────────┘
For teams building on Twenty CRM:
- Twenty’s REST API is at
/api/(not/services/data/v{n}/), so an adapter layer is required - The Twenty GraphQL API at
/api/graphqlcan serve as the back-end for a Salesforce-REST adapter - Recommended adapter stack: Node/Fastify shim that translates SF REST requests → Twenty GraphQL mutations/queries
- SOQL translation: parse with jsforce AST → emit Twenty GraphQL or raw Postgres SQL via Twenty’s metadata API
The shim approach is the right call for Twenty because:
- Twenty’s object model (metadata-driven, arbitrary custom objects) maps cleanly to Salesforce’s sObject model
- The ID format differs (Twenty uses UUIDs, Salesforce uses 15/18-char base-62 IDs) — the shim handles ID translation
- Twenty’s relationship model (FK with metadata-defined edges) maps to Salesforce’s relationship fields
Summary Reference Tables
Cost Comparison at 50 Users (Annual)
| Platform | License Cost | Infrastructure | Est. Total |
|---|---|---|---|
| Salesforce Sales Cloud Enterprise | $99,000 | $0 (SaaS) | $99,000–$180,000+ |
| HubSpot Sales Hub Pro | $60,000 | $0 (SaaS) | $60,000 |
| Pipedrive Professional | $35,400 | $0 (SaaS) | $35,400 |
| Zoho CRM Enterprise | $24,000 | $0 (SaaS) | $24,000 |
| Twenty CRM (Cloud) | $5,400 | $0 (managed) | $5,400 |
| Twenty CRM (Self-hosted) | $0 | $2,000–$8,000/yr | $2,000–$8,000 |
| SuiteCRM (Self-hosted) | $0 | $2,000–$6,000/yr | $2,000–$6,000 |
License cost only. Does not include admin labor, integration costs, or training.
Open Source CRM Comparison
| Tool | Stack | API | Stars (2026) | Best For |
|---|---|---|---|---|
| Twenty CRM | TypeScript/NestJS/React/Postgres | REST + GraphQL | 44,000+ | Engineering-led teams, modern UX |
| SuiteCRM | PHP 8/MySQL | REST (limited) | ~4,000 | Feature parity with SF, enterprise modules |
| Erxes | TypeScript/Node/MongoDB | GraphQL | ~15,000 | Unified sales + marketing + support |
| Odoo CRM | Python/Postgres | JSON-RPC | ~35,000 | ERP-integrated, broad module ecosystem |
Proxy Tool Decision Guide
| Scenario | Recommended Tool |
|---|---|
| Capture production traffic corpus without code changes | Hoverfly (proxy mode) or nginx + OpenResty |
| Java integration test suite with rich stub matching | WireMock |
| OpenAPI spec conformance validation | Schemathesis + Prism |
| Credential injection at network layer (K8s) | Envoy Gateway |
| Request/response transformation gateway | Kong with transform plugin |
| Simple Node.js shim adapter (SF REST → custom CRM) | Fastify + jsforce SOQL parser |
Sources
- GitHub — twentyhq/twenty
- Twenty CRM — Official Site
- Salesforce Bulk API 2.0 Developer Guide
- Generating an OpenAPI 3.0 Document for sObjects REST API (Beta)
- Salesforce Pub/Sub API — Developers
- Streaming API Developer Guide v67.0
- WireMock — Record and Playback
- Hoverfly — The CheckOps Engineer
- Envoy Gateway — Credential Injection
- Salesforce Implementation Cost Breakdown 2026 — folio3
- Salesforce Data Migration 2026 Playbook — OutrightCRM
- Salesforce API Complete Guide 2026 — Unified.to
- Salesforce Summer '25 API v64.0 Updates — conemis
- Best Open Source CRM 2026 — Dench Blog
- Salesforce vs Zoho vs HubSpot vs Pipedrive 2026 — Salesflare