Case study
Two Production HubSpot Portals, Merged into One
The pilot migrated ~11,000 contacts, ~250 deals and ~25,000 emails and notes across 10 companies, with zero mis-associated activities after verification.
- Project
- HubSpot portal merge
- Industry
- CRM / Portal migration
- Role
- Developer: migration engine, rules and tooling
- Node.js
- Express
- MongoDB
- HubSpot v3/v4 APIs
- React
The context
A business ran two production HubSpot portals, one for sales and one for marketing, and needed them merged into a single target portal. The whole run had to be rehearsed end to end in a sandbox before anything touched the real portal.
The problem
The hard part wasn't moving records. It was moving them so that every contact still sat under the right company, every deal kept its line items, and every email, call and note stayed attached to the person it was about.
My responsibility
- Built a dedicated migration engine in Node.js, Express and MongoDB against the HubSpot v3/v4 APIs, moving companies, contacts, deals, line items, notes, emails, calls, meetings and attachments with per-record mapping and provenance tracking
- Built a merge rules engine driven by a spreadsheet the client maintains: field-level source, target or skip rules, blank-value handling, and a dormancy rule for records with no activity since a cutoff date
- Added data-quality safeguards: properties matched by label as well as internal name, and dropdown values, owners, teams, pipelines and stages translated between portals
- Built the operations tooling: a React dashboard to start, monitor and resume runs, plus repair, reconcile and cleanup scripts so any run can be repeated safely
Architecture
- Seed companiesPicked by domain
- CascadeOne hop out
- Merge rulesThe client's spreadsheet
- Ordered writeHubSpot v3/v4 API; rejected writes are parsed and retried
- Target portalOne CRM
- ReconcileSource = target
Key decisions
A migration engine, not CSV imports
Every record is mapped individually and its origin tracked, so any run can be audited, repaired or resumed.
A scoped cascade in dependency order
Seed companies are picked by domain, everything linked to them is pulled one hop out, and records are written in dependency order so associations always have something to point at.
Rules the client owns
Merge rules live in a spreadsheet the client maintains: source, target or skip per field, blank-value handling, and a dormancy rule that drops records with no activity since a cutoff date.
Rehearse before cut-over
The whole run was rehearsed end to end in a sandbox before anything touched the real portal.
Technical challenges
1.Writes HubSpot rejects
A parser reads HubSpot's own rejection errors and retries without the offending property or value, instead of losing the whole batch.
2.Emails and notes with repeated subjects
Bulk-created emails and notes are matched back on a composite key, so they land on the right contact even when subjects repeat.
3.Two portals that name things differently
Properties are matched by label as well as internal name, and dropdown values, owners, teams, pipelines and stages are translated between portals.
Results
- ~11,000 contacts, ~250 deals and ~25,000 emails and notes migrated in the pilot, across 10 companies
- Zero mis-associated activities after verification
- Audit reports reconciled source and target counts for the client, so the result could be checked rather than taken on trust