Case study · Life Time · 2022 to 2025

Life Time's first Vue 3 page, then the sign-up flow behind the digital guest pass

I started by rewriting Find a Location, the page people use to find a club, as the first Vue 3 code on Life Time's public website. Then I led frontend on the Account team, where I wrote the web code behind the digital guest pass and handled sign-ups for hundreds of thousands of new accounts.

Role
Principal Software Engineer (contractor)
Team
On the Account team, me leading frontend, a junior frontend engineer, a senior backend developer, a PM, and QA
Stack
Vue 3, Vue 2, TypeScript, Pinia, Google Maps, Vitest, Playwright, Datadog

Find a Location, rebuilt in Vue 3

The old page was jQuery. The project had three goals: retire the jQuery code, move to a new and very different visual design, and work well on phones. The organization had already chosen Vue 3 as its direction, and I was the one who knew it. The page is still live and, as of September 2026, looks the way it did when I shipped it.

Find a Location on a phone: a map of Minneapolis clubs with a row of club cards along the bottom and a List button to switch views.
On a phone, the map is its own view, with results as a row of cards.

An HTML and CSS specialist built the new design as static markup. I made it a working app: search with Google Places autocomplete, club results from existing APIs, amenity filters, a custom-styled Google Map, and a phone layout where the list and map become separate views. I called the Google Maps JavaScript API directly instead of using a Vue wrapper library, so I could reuse patterns that already worked. The app runs inside the site's Adobe Experience Manager pages, next to the Vue 2 build the rest of the site still loads.

One state change, two ways to select a club

The map and the list had to stay in step. Clicking a marker scrolls the list to that club, and clicking a row does the same thing as clicking its marker. I built this around shared Pinia state: the marker and the row each set the same selected club in the store, and everything else reacts to that one change. It doesn't matter which one you click, because both reach the same end state through the same code.

Two inputs, one selected clubClicking a map marker or a list row sets the same selected club in a Pinia store. The store change scrolls the list, highlights the row, enlarges the marker, and opens the pop-up.Map markerclickList rowclickPinia storeselectedClubScroll listHighlight rowEnlarge markerOpen pop-up
Find a Location on desktop: the Fridley club is selected, so its map marker shows the Life Time logo and an address pop-up, and its row in the results list is highlighted with the same logo.
The shipped page with Fridley selected: logo marker and pop-up on the map, highlighted row in the list.

I wrote unit tests for the Vue logic in Vitest and a few Playwright tests that click through from a result to a club's page. The launch had no deployment problems.

The digital guest pass

Members used to bring a guest to the front desk, where the guest filled out paperwork and a waiver. Members got two passes a month. Life Time announced the digital guest pass in May 2024, about four months after I joined the Account team. The idea came from marketing and business development, and I wrote the web code that made it work.

A member texts a pass link from the app. The link tells our page who invited the guest. We ask for the guest's email first: an existing account goes to log in, and anyone else goes to sign up. The new account records whose guest it is, and when the guest logs in to the app, the app reads that and shows a QR code that gets them into the club.

The digital guest pass flowA member texts a pass link from the app. The guest lands on the sign-up page, which checks their email and sends them to log in or sign up. The new account records the invite. The guest logs in to the app and gets a QR code. The middle three steps are the web code I wrote.Member texts a pass linkfrom the Life Time appGuest lands on the sign-up pagewelcoming them as a guestEmail checkexisting account: log in; new: sign upNew account records the invitethe member's ID travels with itGuest logs in to the appand shows a QR code at the deskMy code

Most of the tricky work was handling users in different states, starting with people who already had accounts. The handoff to the app turned out simple, since the identifier traveled from the text message, through our page and the new account, to the app at login.

Every guest now needed a free account, which fed Life Time's push to grow its free LT Digital subscription. LT Digital passed 1 million subscribers by October 2024 and 1.7 million by February 2025, around when my contract ended. My code handled the sign-ups for hundreds of thousands of them. (Life Time's public numbers don't say how many came through guest passes.)

The same flow for race packet pickup

In fall 2024, I generalized the flow for Life Time's athletic events, starting with race packet pickup. Participants get an email whose link carries the event, our sign-up page customizes itself for that event, the account records it, and the app shows a QR code for the pickup line. I made the page data-driven: a map from event ID to welcome text and branding, so adding an event meant adding an entry, not writing a new page.

The Life Time Miami Marathon and Half in February 2025 was one of the first events to use it, with a fast-track app lane at bib pickup. The Life Time Chicago Half Marathon now requires all participants to use the app for packet pickup.

Frontend errors that showed up before anyone reported them

I brought Datadog browser logs to the account section, with logging and proper error handling throughout, so exceptions in the UI (including unhandled ones we'd never have seen) showed up in Datadog. Alerts in Microsoft Teams told us when things looked abnormal. Many turned out to be API failures, not bugs in our code, and because the UI saw them first, we were often the ones telling other teams about problems in their APIs.

Retrying account creation under load

The most perplexing alerts were account-creation failures that came in spurts, worst right after Life Time emailed a race's participants and invited them all to sign up at once. Life Time learned to stagger those emails. The backend couldn't simply be changed, so I did what the frontend could: up to seven attempts with exponential backoff, about two minutes of spinner in the worst case, then a message to try again later. The backend team confirmed a retry couldn't create a duplicate account.

What I'd do differently

I didn't add jitter, a small random delay on each retry that keeps thousands of clients from retrying at the same moment. I'd add it next time. More to the point, retries were a band-aid. The real fix belonged on the backend, such as a queue to spread out sign-ups during a spike.

Along the way

  • My first months were cleaning up older JavaScript that loaded on every page of the member and marketing sites. I traced what actually ran, deleted the dead code, and replaced jQuery with native JavaScript, leaving a little in one section already slated for a rewrite.
  • Without being asked, I reviewed pull requests across every frontend repo in the organization, pushing for modern JavaScript and Vue code that would carry over to Vue 3. Most of the developers came from backend work and often took the suggestions.
  • On the Account team, I mentored our junior frontend engineer, who took over the account frontend when I left.