Designed and owned a serverless integration platform on AWS for a 20+ brand hospitality portfolio running on a single HubSpot CRM. I built and ran multiple production integrations across scheduled and webhook patterns, managed live sync state in DynamoDB, led incident response on two production data bugs, and cut stale infrastructure out of the account.
The group runs 20+ hospitality, marine, and real-estate brands on a
single HubSpot CRM and a single AWS account in
us-east-1. Each brand ran different source systems, and
none of them fed HubSpot on their own.
I was the technical owner of the integration layer that connected them. Over the co-op I built and ran six production serverless integrations, moving contact and deal data between HubSpot and three source systems (Molo, Tripleseat, and D-Tools) and running a deduplication engine over HubSpot's own 56k+ contacts. No other engineer was on the team, so I owned scoping, build, deploy, incident response, and the handoff documentation.
Every integration lands data in the same HubSpot portal, but the right way to get it there depends on the source. I used two patterns, chosen per system.
EventBridge triggers the Molo sync Lambda every 12
hours. The Lambda pulls contacts and vessels for three marinas, compares
each record against state in DynamoDB, and pushes only
what changed. The two Tripleseat lead syncs, for Harbor Lights and
Basecamp, use the same scheduled pattern on a 5-minute poll.
API Gateway, which proxies the raw event to a Lambda:
the Tripleseat deal-sync Lambda turns Tripleseat bookings into
HubSpot deals, and the D-Tools sync Lambda turns D-Tools clients
into HubSpot contacts. The Lambda fetches the full record, then
creates or updates in HubSpot.
S3. It defaults to a dry-run gate, so the merge API
is never called unless a run is explicitly flipped to live, because
merges are irreversible.
The scheduled Molo sync is the most stateful integration. A third-party developer wrote the reverse-engineered Molo auth client; I built everything around it: the AWS orchestration, the DynamoDB state design, the change-detection, and the merge logic.
Each run computes an MD5 hash of a contact's synced fields and
compares it to the last hash stored in DynamoDB.
Unchanged contacts are skipped, so a run that pulls ~4,600 contacts
writes only the handful that actually changed. That keeps the sync
idempotent and well inside HubSpot's rate limits.
Writes go through a three-tier merge so an automated sync never overwrites human-entered data.
hs_all_assigned_business_unit_ids and
marina_brand_interest get the new value appended to
whatever is already there, so a contact keeps every brand and
interest it has ever had.
hubspot_owner_id, hs_lead_status, and
lifecyclestage are written only when the field is
blank, so the sync never reassigns a contact a human already
touched.
The core of each Molo run is small. Hash the record, compare it against DynamoDB, skip if nothing changed, otherwise merge into HubSpot and persist the new state for next time.
The same append and set-if-empty merge rules are the default for every integration on the platform, not just Molo. An automated write should never be able to destroy data a person entered by hand.
UPDATE_BOOKING and
UPDATE_EVENT webhooks at the same time, both Lambda
invocations searched HubSpot for the deal, both found nothing,
and both created one. I fixed it at the source by removing the
Update Event trigger from the webhook, so exactly one webhook
fires per booking action and the dedup search stays
authoritative.
A previous vendor had left two legacy Tripleseat sync Lambdas — a webhook handler and a queue worker — running in the account. Once my replacement was confirmed working, I disabled both by setting Reserved Concurrency to 0 rather than deleting them, so they stopped executing but stayed recoverable pending owner sign-off.
Across the platform I kept the operational surface tight. Each
integration is an isolated Lambda with its own credentials and
environment, IAM roles are scoped to least privilege (for example
secretsmanager:GetSecretValue limited to the
waterside/* prefix), credentials move toward
Secrets Manager, and every function logs to
CloudWatch for debugging.
Each integration is a versioned Lambda deployed from a single-purpose codebase, with configuration in environment variables and credentials in Secrets Manager. Deploys were manual through the console during the co-op — moving the stack to infrastructure-as-code is the next thing I'd build, and the incident below is the reason why.
The account had no non-production environment when I inherited it. That cost me: an early Molo test run overwrote fields on 7 live contacts, which I restored by hand from HubSpot's property history. The three-tier merge logic described above exists because of that incident. The more complete fix would have been a staging portal to test against, not just better merge rules — that's what I'd add first if I rebuilt this.
Detection is log-based, not alarm-based: every function logs to CloudWatch, and I read those logs to confirm each sync ran clean. There's no alarm today that would catch a silent stall in the 12-hour Molo sync or the 5-minute Tripleseat poll. Alarms on invocation count, error rate, and sync-record delta, wired to a notification target, are what I'd add next.
Reversibility is handled deliberately in two places instead: the dedup engine defaults to a dry-run gate because merges are irreversible, and the legacy vendor Lambdas were disabled via reserved concurrency 0 rather than deletion, so they stayed recoverable pending sign-off.