CourtReserve to HubSpot for a 16-location pickleball franchise, then handed over
A 16-location pickleball club franchise
- Sports and recreation
- Canada
- Integration, handed over in 2026
- Locations in the nightly run
- 15
- Record types synced per club
- 6
- Contacts given visit rollups
- 91,697
- Live since
- April 2026
- Ownership transferred
- September 2026
A pickleball club franchise was opening roughly one location a month and could not see a member’s play history anywhere. Court bookings, events, memberships and payments lived in a booking platform that gives every club its own account and its own credential. We built a nightly one way sync that carries 6 record types per club into HubSpot, resolves a member who plays at several clubs into a single contact, then recomputes visit and spend rollups. The franchise owns and runs it now.
01 The Challenge
The franchise was growing quickly, and every club ran its own bookings, events and memberships in the same court management platform. Head office could not answer two basic questions: what a member is worth, and when they last played. The answer sat in one account per club, with no view across them.
The platform’s API set the shape of anything we could build. Each club is a separate organization with its own credential, so there is no single tenant wide key to hold. A request may cover at most 31 days, so a historical backfill is a loop over windows rather than one call. There are no webhooks, no bulk endpoints and no test environment, so a nightly poll against production is the only honest design. On the HubSpot side, email is globally unique, so a person who plays at three clubs cannot be three contacts, and yet the franchise needed to see all three clubs on that one record. The operator also wanted to own the system at the end of it, which ruled out anything only we could run.
02 The Solution
The sync runs once a night, one club at a time. For each club it pulls the last 2 days of members, reservations, events, event registrations, memberships and transactions, fetches the HubSpot records it already knows about, then writes.
Members become contacts, upserted on email inside the club’s own scope, falling back to the platform’s member id where a member has no email address. Three properties carry the multi-location problem. A multi-select holds every club a member has played at and is merged rather than replaced, a single select holds the primary club and is set only on creation, and a third records which club synced the contact last. When a batch create hits the global email uniqueness rule, the handler re-queries the existing contact, merges the club set into it and patches. The second club to sync a shared member adds to the record instead of failing against it.
Reservations, events, event registrations and memberships each become a custom object with its own dedupe key written as a property, and transactions become deals. Associations run as a separate pass after the writes, so no record waits on the thing it points at. Rate limiting is deliberate: one club at a time, a floor on the interval between requests, and a retry that honours the platform’s own backoff header. A portal allowlist fails the run closed when the connected portal is not the one the environment expects. After the writes land, a second phase recomputes first visit, last visit, visits in the last 30 days and monthly spend on every contact, so the rollups always describe data that was just synced.
03 The Outcome
The sync went live in April 2026 across 15 club locations. The first full rollup pass patched 91,697 contacts with first visit, last visit, visits in the last 30 days and monthly spend, which is the play history the franchise could not see at all before. A member who plays at more than one club is one contact carrying every club they play at, which is what makes a head office report possible.
One HubSpot limit shaped the consent design. Marketable status is read only through the API, so the opt out flag from the source drives a HubSpot workflow rather than being written onto the contact directly. Day to day operation is a single check: a healthy night ends with a clean exit for the sync and a clean exit for the rollups, in two dated log files. Ownership transferred in September 2026. The franchise holds the server, the repository and the tokens, and the server we had been running was destroyed after 8 days of green runs on theirs.
How the lifecycle runs now
-
At the club
Each club's own booking account
- Source: Club's booking account, one per location Next: Members book courts and register for events.
- Members book courts and register for events Next: Memberships and payments recorded.
- Memberships and payments recorded Next: Reads the last 2 days of six record types, in Nightly sync.
-
Nightly sync
Sync engine, one club at a time
-
In CourtReserve: Reads the last 2 days of six record types Next: Connected portal the expected one?. - Decision: Connected portal the expected one? Yes: Upsert on club scope and email, or member id, in One member record. No: Run fails closed.
- Outcome, stops: Run fails closed
-
-
One member record
HubSpot contact, one per person
-
In HubSpot: Upsert on club scope and email, or member id Next: Email already held by a contact?.
- Decision: Email already held by a contact? No: New contact, primary club set once. Yes: Re-query, merge the club set, patch.
-
In HubSpot: New contact, primary club set once Next: Opt out flag drives a HubSpot workflow.
- Re-query, merge the club set, patch Next: Opt out flag drives a HubSpot workflow.
-
Opt out flag drives a HubSpot workflow Next: Four record types become custom objects, in Play history.
-
-
Play history
Custom objects and deals
-
In HubSpot: Four record types become custom objects Next: Transactions become deals.
-
In HubSpot: Transactions become deals Next: Associations run as a separate pass.
- Associations run as a separate pass Next: Visit and spend rollups patched onto every contact, in Head office view.
-
-
Head office view
Rollup pass, after a successful sync
-
In HubSpot: Visit and spend rollups patched onto every contact Next: Play history visible across every club.
- Outcome, done: Play history visible across every club
-
04 Stack
One small cloud virtual machine in a Canadian region, running Node 22 and TypeScript directly with no build step. A single cron entry at 02:00 Eastern chains the sync and then the rollups. Run state, cursors and checkpoints sit on disk beside the code, and each phase writes its own dated log with a two week retention.
Locations, owners and teams are configuration files rather than code, so onboarding a club is a credential, a target entry, an owner mapping and a backfill run. None of it needs a release. That choice is also what made the handover simple, because the whole system is a repository, a server image and two tokens.
05 Architecture and data model
The sync engine is the only component that talks to both sides, and it talks to them in one direction. It reads each club’s account in turn, maps every record into a HubSpot payload, writes contacts first and then the objects that associate to them, and keeps its cursors and checkpoints on disk so an interrupted night resumes rather than restarts. The rollup pass runs only after a successful sync, then patches the visit and spend properties back onto contacts.
Every object carries its own dedupe key as a property, which is what makes the nightly re-run idempotent. The same reservation seen on two consecutive nights updates one record rather than creating two. Each key is built from the source record’s own identifiers, never from a name or a date.
| Source record | HubSpot object | Direction | Dedupe key |
|---|---|---|---|
| Member | Contact | To HubSpot | Club scope and email, falling back to member id |
| Reservation | Custom object | To HubSpot | Reservation key |
| Event | Custom object | To HubSpot | Event key |
| Event registration | Custom object | To HubSpot | Event registration key |
| Membership | Custom object | To HubSpot | Membership key |
| Transaction | Deal | To HubSpot | Transaction key |
The contact key is the one exception to that pattern. It is the club’s organization scope combined with the email address, falling back to the member id where there is no email. The scope is what lets two clubs describe the same person without either of them owning the record, while HubSpot still keeps exactly one contact for that person.
- Hosting
- A single small cloud virtual machine
- Runtime
- Node and TypeScript, run under cron with no build step
- Cadence
- Nightly at 02:00 Eastern, a 2 day window per run
- Pattern
- Polling on a watermark
- HubSpot
- Sales Hub Enterprise, Marketing Hub Professional
What we would do again, and differently
What we would do again
- Give every object its own dedupe key and write it as a property, so a re-run is always safe.
- Keep locations, owners and teams in configuration files, so onboarding a club needs no release.
What we would do differently
- Build the multi-location merge before the first backfill, not after the first conflict.
- Plan the handover in week one, so the server, the repository and the credentials are transferable by design rather than by effort.
Systems in this build
Partnerships and accreditations
HubSpot accreditations
Industry specializations
Partnerships
Have a system in the same shape?
Send the systems, the direction and the record volume. We reply with a scope and an effort band within one business day.
Thanks, we will reply within one business day
Prash or Sean reads every one of these and comes back with a scope, or with the one question we need answered first.