Work
Operations LIVE IN PRODUCTION AT DRIV.AUTOSTEREA.COM

Two gym apps that finally talk to each other, no more double entry

We built and run a sync that makes PushPress and GoHighLevel behave like one system. Every member, tag, and pipeline stage stays in step on its own. About 40 hours a month back.

~40 hrs/moowner time handed back, zero double-entry
Book a call Visit it live

The problem

DRiV Fitness, a CrossFit gym in St. Augustine FL, ran on two tools that did not talk to each other. PushPress held the membership data: who signed up, who is on a free trial, who paused, who canceled, who checked in to class. GoHighLevel ran the marketing: pipelines, tags, automated emails and texts. Keeping them in step meant re-typing the same people into both, over and over. Every new lead, every free-trial signup, every membership, every hold and cancel was a manual copy. Slow, easy to get wrong, and it made the owner the integration. So we built one piece in the middle: a sync that listens to PushPress and keeps GoHighLevel current on its own. PushPress stays the single source of truth for membership. GoHighLevel keeps doing marketing. Todd stops re-keying anything.

What we built

The modules of the system.

01

The sync service

A small always-on service that sits between PushPress and GoHighLevel. PushPress fires a webhook the moment something changes (a new customer, a free-trial purchase, an enrollment, a status change, a check-in, a class reservation), the service catches it and writes the result into GoHighLevel. Seven event types are handled end to end. Todd never opens both tools to keep them matched. It just stays matched.

02

Email as the shared key

Both platforms know a person by their email. The service matches on it, then caches the PushPress-to-GoHighLevel link so the next event for that person skips the lookup. A member in PushPress lands on the right GoHighLevel contact every time, with no duplicates and no guessing which record is the real one. The PushPress ID is also written back onto the GoHighLevel contact as a custom field.

03

Lifecycle tags that swap themselves

The gym's whole lifecycle is encoded as a set of mutually exclusive pp- tags: free trial created, free trial completed, membership active, drop-in active, kids active, online-programming active, on hold, cancel requested, canceled. When the service sets the new tag it strips the old ones, so a contact is only ever in one state. The gym's GoHighLevel workflows (built by Todd's marketer) just listen for a tag and fire the right email or text.

04

Three pipelines, routed by plan

The service reads the PushPress plan on each enrollment and routes it to the right place: a Sales pipeline (New Lead, Free Trial Booked, Free Trial Completed, Member), a Members pipeline (Active, On Hold, Cancel Requested, Cancelled, plus online-programming and personal-training stages), and a separate Drop-In pipeline (Purchased, Booked, Attended, Post-Class Follow-Up). Drop-ins and kids never leak into the member onboarding sequence, because they are not members.

05

Check-in tracking

Every class check-in updates a Last Check-in Date and increments a Total Check-ins counter on the GoHighLevel contact, so the marketing side always knows how engaged a member is without anyone touching it. A drop-in's first real arrival also fires a welcome trigger, once, so the welcome email only goes out after they actually show up.

06

Status dashboard

driv.autosterea.com serves a live status page: online state, uptime, synced-contact count, and a per-event summary table (total, successes, errors, last event). Small JSON endpoints (/api/status, /api/events, /health) back it. It is the at-a-glance proof that the bridge is healthy and processing.

How it fits together

One small Node service runs the whole bridge. PushPress sends a webhook to /hook/pushpress whenever something changes. The service answers PushPress instantly (200 OK) and then does the real work asynchronously, so PushPress never sees a slow response and never retries. Each event is routed to a dedicated handler: customer, enrollment, check-in, or reservation. The handler matches the person to a GoHighLevel contact by email (creating one if needed), then writes tags, moves the right pipeline stage, and updates custom fields through the GoHighLevel v2 API. PushPress is the source of truth for membership; GoHighLevel handles marketing, and data flows one way to keep that split clean. State (the PushPress-to-GoHighLevel contact map, opportunity map, and an event log) lives in a single debounced JSON file on the box, so there is no database to babysit. The whole thing is plain TypeScript on Node and Express with native fetch and one runtime dependency (Express). It runs under systemd behind Caddy (auto-TLS) on a shared DigitalOcean VPS at driv.autosterea.com, alongside our other services but on its own port. We built it and we run it.

NodeExpressTypeScriptnative fetchJSON file storePushPress API v3GoHighLevel API v2Webhooks (HMAC-SHA256)systemdCaddyDigitalOcean VPS

Under the hood

The decisions that mattered.

A bridge, not a bigger tool

DRiV did not need to throw out PushPress or GoHighLevel. Both do their jobs well. The problem was the gap between them, and Todd standing in that gap re-typing people. So we built one part to close the gap instead of replacing two working tools. PushPress stays the system of record for membership. GoHighLevel stays the marketing engine. The sync keeps them honest with each other automatically. The honest engineering version: the owner was the integration, and we replaced the owner with about 250 lines of routing logic.

Answer fast, work slow

Webhooks are unforgiving. If you take too long to respond, PushPress assumes failure and retries, and now you are processing the same event twice. So the service replies 200 OK the instant a webhook lands, then does all the GoHighLevel work after. The handler chain (find-or-create contact, swap tags, move pipeline, set custom fields) can take several API round-trips, and none of that holds up the response. PushPress sees a healthy, instant endpoint every time, which is what keeps the retry storm from ever starting.

Plan categories, so the wrong people never get the wrong email

A gym's plans are messier than they look: free trials, real memberships, ten-class packs, personal training, kids programs, drop-ins, online programming, a specialty GXRL program, staff comps, and deprecated plans that should be ignored. Every PushPress plan ID is mapped to a category in one config block, and the category decides everything downstream: which lifecycle tag, which pipeline, whether it gets a pipeline at all. A drop-in is tagged pp-drop-in-active and lands in the Drop-In pipeline, never the member welcome sequence. Staff and deprecated plans are skipped silently. And if a brand-new uncategorized plan ever appears, the service refuses to treat that person as a member, syncs only their identity, and alerts us to add the plan, rather than quietly dropping them into onboarding.

Idempotent by design, because webhooks repeat

The same event can arrive more than once, and a member can move through the same stage twice (sign up, cancel, come back). So nothing in the service assumes it is running for the first time. Setting a pipeline stage checks a local opportunity cache, then searches GoHighLevel for an existing opportunity, then creates one only if there genuinely is not one, and even recovers from GoHighLevel's duplicate error by finding and updating the existing record. Tags swap rather than pile up. The drop-in welcome fires once per cycle and resets on the next purchase. Run any event twice and the result is the same as running it once, which is the only way an unattended bridge stays trustworthy.

Built to run unattended

This is not a script someone kicks off by hand. It runs as a service under systemd, so it restarts on its own and comes back after a reboot, with a graceful shutdown that flushes state to disk first. Caddy puts it behind a clean, auto-TLS address. State is a single JSON file the service debounces to disk, so there is no database to maintain and the event log self-trims. A status page at driv.autosterea.com shows uptime, synced contacts, and per-event success and error counts at a glance. We operate it in production, so Todd just gets the result: two tools that act like one.

Release log

What we shipped.

Discovery, build, deploy

Verified both APIs, pulled live data from PushPress and GoHighLevel, built the sync service (Express + TypeScript), created the GoHighLevel custom fields, registered six PushPress webhooks, and shipped it to the VPS under systemd behind Caddy. First real member synced end to end and confirmed in GoHighLevel.

Lifecycle and pipelines wired

Encoded the full gym lifecycle as mutually exclusive pp- tags and routed enrollments into the Sales and Members pipelines, so the marketer's GoHighLevel workflows fire off a single tag with no filters.

Drop-ins, kids, online and specialty split out

Added plan-category routing and a dedicated Drop-In pipeline so non-members never enter member onboarding. Added a reservation handler (Purchased to Booked) and a once-per-cycle drop-in welcome trigger on first check-in.

Safety nets

Added an unknown-plan guard that refuses to treat uncategorized plans as memberships and alerts us instead, an ignored-plan list for deprecated plans, and end-date sync so cancel and hold workflows can tell members exactly when their change takes effect.

Running it

The service runs unattended under systemd behind Caddy at driv.autosterea.com, with a live status dashboard. We keep it running.

The outcome

Two platforms now behave like one. PushPress is the single source of truth, GoHighLevel stays current on its own, and nobody re-keys a thing. New leads, free trials, memberships, holds, cancels, drop-ins, kids, and every class check-in flow into the right pipeline and the right tag automatically, which lets the gym's marketing automations run without a human feeding them. That gave the owner about 40 hours a month back. In Todd O's words, it has been a great help managing the gym's digital needs. We built the bridge, and we run it. Your business, on autopilot.

Put your business on autopilot