Saaspocalypse
The migration runbook

Migrating off Salesforce — phase by phase.

Salesforce migrations live or die on two technical details: Bulk API 2.0 for the data extraction and a deliberate strategy for the 18-character record IDs that all your historical integrations point at. We preserve those IDs as a column in the new schema so reports, webhooks and saved searches keep resolving.

10-14 weeks end-to-endFixed priceParallel run before cutoverYou own the code

The phases, tuned to Salesforce

Each phase below is the Salesforce-specific version of our standard playbook. Same shape every engagement, different specifics per SaaS.

  1. Audit & feature scoping

    1-2 weeks

    We map the 10-20% of Salesforce your team actually uses — every object, every active workflow, every report your CRO opens. The 80% nobody touches becomes the cost-saving case.

    Shadow sales, ops and leadership on real tasks
    Pull a 90-day login + feature-usage report from Salesforce Optimizer
    Inventory custom objects, fields, validation rules, Apex triggers, Flows
    Identify integrations and inbound webhooks we need to preserve
    Map who-can-see-what (Sharing Rules + Profiles + Permission Sets) into a tractable RBAC model
    You walk away with

    A Scope Audit naming every object that stays, every workflow that goes, and a mapping of Sharing Rules → roles.

  2. Schema design & ID strategy

    1 week

    We model your data in Postgres, preserving the 18-character Salesforce IDs as a column on every record so historical links and external systems keep resolving. Custom fields get sensible types instead of strings-everywhere.

    Design the Postgres schema (named tables, real types, real foreign keys)
    Add a `salesforce_id` column on every migrated table (indexed)
    Document the picklist mapping (Schema.PicklistEntry API names vs. labels)
    Decide RBAC: 3-4 named roles in code, replacing the Sharing Rules + Profiles + Permission Sets stack
    Architecture Decision Record + DPA signed
    You walk away with

    A signed ADR, schema migrations checked into git, and a documented ID-preservation strategy.

  3. Extract via Bulk API 2.0

    1-2 weeks

    We pull every record you care about through Bulk API 2.0 — chunked, async, restartable. Soft-deleted records inside the 15-day Recycle Bin window get a separate pass.

    Authenticate via OAuth 2.0 (client-credentials or JWT bearer for unattended)
    Submit Bulk API 2.0 query jobs per object, poll for completion, download CSV
    Pull Einstein Activity Capture data separately (it lives on AWS, not in the Salesforce core)
    Backfill custom-object relationships via REST Composite
    Extract metadata (custom fields, picklists, validation rules) via the Tooling API for reference
    You walk away with

    A re-runnable extract script + a reconciliation report comparing record counts to Salesforce dashboards.

  4. Build + weekly demos

    5-8 weeks

    Working software every Friday. Your reps log in to staging from week two, use it on real deals from week four. By cutover they've used the new product more than they used Salesforce.

    Build the leads, opportunities and accounts views — the 8-10 fields your team actually fills in, no more
    Pipeline as a Kanban with stage transitions as plain buttons (no validation-rule mazes)
    Activity logging baked into the views (no tab-switch, no record search)
    Email sync via Gmail / Microsoft Graph (replaces Salesforce Inbox)
    Dashboards rendered server-side from real SQL — no 2,000-row cap, no cache staleness
    You walk away with

    A production-grade application running on your own infrastructure, with you as the GitHub owner.

  5. Parallel run + reconciliation

    2-3 weeks

    Both systems live. Your team works in the new product; Salesforce stays read-only as a fallback. Nightly reconciliation reports compare totals and flag drift.

    Daily incremental sync from Salesforce into the new system (last-modified watermark)
    Nightly reconciliation: record counts, sum-of-£-by-stage, won-lost-by-rep
    Train reps in two 60-minute sessions, recorded for the rest
    Re-route inbound webhooks (Stripe, Calendly, etc.) one at a time with a kill switch
    Confirm every external integration resolves against the preserved `salesforce_id`
    You walk away with

    A reconciliation dashboard showing zero drift for seven consecutive days + a signed sign-off from sales leadership.

  6. Cutover + Salesforce read-only

    1 day + 90-day fallback

    DNS and SSO flip on a Tuesday morning. We sit on a war-room call. Salesforce stays read-only for 90 days as a safety net; auto-renewal cancelled the same day.

    Smoke-test every critical workflow before flipping
    Cut SSO to the new app; redirect bookmarks via a one-line proxy
    Lock Salesforce to read-only (revoke write profiles)
    Cancel Salesforce auto-renewal in writing
    Hold a 90-day read-only window for compliance / dispute / regret
    You walk away with

    A Cutover Runbook + a Day-1 Status Report + a quiet inbox where the renewal email used to land.

The Salesforce APIs we'd actually hit

Concrete endpoints we use to extract your data during the parallel-run phase. We keep the extract scripts — they're useful at every renewal.

OAuth 2.0 token
POST /services/oauth2/token
Authenticate the migration tooling (JWT bearer or client_credentials grant)
Bulk API 2.0 — create query job
POST /services/data/v62.0/jobs/query
Kick off async bulk extraction per object (Accounts, Opportunities, Contacts, custom objects)
Bulk API 2.0 — get results
GET /services/data/v62.0/jobs/query/{jobId}/results
Download the CSV chunks once the query job is complete
SOQL query
GET /services/data/v62.0/query/?q={SOQL}
Quick lookups, sample exports, reconciliation totals during parallel run
REST sObject CRUD
GET /services/data/v62.0/sobjects/{SObject}/{id}
Individual record fetches and live syncing during parallel run
Composite tree
POST /services/data/v62.0/composite/tree/{SObject}
Backfill parent/child object graphs (e.g. Opportunity → OpportunityLineItem) in one request
Tooling API metadata
GET /services/data/v62.0/tooling/sobjects/CustomField/{id}
Extract custom field definitions, picklist values, validation rules for the schema mapping
Connect REST API — Chatter feed
GET /services/data/v62.0/chatter/feeds/record/{recordId}/feed-elements
Pull internal Chatter activity if it's in scope (most teams skip this)

The Salesforce-specific gotchas

Things we've learnt the hard way (or that the documentation understates). We'll surface these on the kickoff call so nobody is surprised in week 6.

What stays where it is

Migrations work because they're scoped honestly. Here's what we don't move — by design.

  • ·Apex triggers and classes — we rewrite the logic in TypeScript with tests
  • ·Visualforce pages and Lightning Web Components — UI gets rebuilt in React
  • ·Salesforce Flow — automation becomes durable jobs (Inngest / BullMQ) with retries and a real audit trail
  • ·AppExchange managed packages — either replaced by native code or dropped if shelfware
  • ·Reports & dashboards inside Salesforce — rebuilt in Metabase on the new Postgres warehouse
  • ·Salesforce CPQ / Billing / Field Service — these are large enough products to merit their own scoping conversations

Get a Salesforce-shaped migration plan.

Tell us your seat count, contract end date, and the integrations you can't lose. We'll come back within a business day with a fixed-price scope and a timeline tuned to your renewal.

Scope a Salesforce migration

One business day. NDA + DPA on request.