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
flyingbridge-molo-hubspot-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:
harborlights-tripleseat-deal-sync turns Tripleseat
bookings into HubSpot deals, and
vineyard-dtools-hubspot-sync 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,
tripleseat-prod-tripleseatWebhook and
tripleseat-prod-queueWorker, 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.