Skip to content

← Work

Grounds

Grounds is a fitness subscription product: members sign up on the web, pay for a membership, and manage it from their own account, with the admin, affiliate, and lifecycle systems the business runs on sitting behind that. Separately, the brand needed a merch experience of its own — a custom storefront built on its existing Shopify catalog and linked from the main Grounds site, rather than a themed Shopify store.

I built the subscription product's surfaces, from the landing page through checkout to the member dashboard and admin panel, and was the primary developer of the merch storefront.

At a glance

  • Role: Built the subscription product's member-facing and internal surfaces; primary developer of the headless Shopify merch storefront.
  • Core constraint: Keep membership billing and merch commerce as separate systems, with Shopify staying the source of truth for catalog, inventory, and checkout.
  • Strongest evidence: A variant-level persistent cart hydrated against Shopify, inventory-driven option selection, and a Shopify-hosted checkout handoff.

Context

The subscription side is a recurring-billing product, which means access depends on a payment relationship that continues over time. Checkout, the member dashboard, payment and profile management, lifecycle email, and affiliate attribution all have to agree about the same membership, and they each read or write it at a different moment in a customer's life.

Merch looks similar but is a different problem. It is a physical catalog with colors, sizes, and finite inventory, bought once rather than subscribed to. Grounds already ran that catalog in Shopify, and the goal was a branded browsing and cart experience on top of it — not a second commerce system. That set the constraints clearly:

  • Products, variants, inventory, and pricing stay in Shopify, which the business already operates in.
  • Payment, tax, shipping, and order creation stay in Shopify too. The storefront's job is the experience in front of them.
  • The cart lives in the browser, so whatever it stores has to be correct on its own and survive a reload without going stale.

My role

On the subscription product I built the member-facing and internal surfaces: the marketing and landing experience, subscription checkout, the dashboard where a member manages their membership, payment method, and profile, and the admin panel behind it. Affiliate tracking, analytics, and the lifecycle integrations were built against that same subscription state rather than bolted onto it separately.

On the merch storefront I was the primary developer. That covered the Shopify integration layer, the collection and product pages, the variant selection behavior, the cart and its drawer, and the handoff into checkout.

Architecture

The subscription product is a Next.js and TypeScript web application. Firebase carries authentication, user data, and storage, and a Node and Express layer on Cloud Functions handles the server-side work around subscriptions and the services that react to them: Stripe for subscription payments, RevenueCat for subscription and revenue events, Klaviyo for lifecycle email, and the affiliate and analytics tracking that attributes a signup back to where it came from.

The two surfaces are deliberately separate systems. A membership and a t-shirt are different commerce problems. Recurring billing needs entitlement that holds over time across a payment relationship; merch needs variants, inventory, and a single purchase that ends. Folding merch into the subscription platform would have meant rebuilding catalog and inventory management that the business already runs on Shopify, and folding subscriptions into Shopify would have meant giving up the billing and lifecycle infrastructure the membership depends on. The two share a brand and a link between them, not a system.

The storefront is a Next.js App Router application that reads its catalog from Shopify's Storefront GraphQL API and keeps its cart in the browser. Everything it renders is Shopify's data; everything it decides is about presentation and about which exact thing a customer selected.

Shopify's API shape stops at the integration layer. Storefront GraphQL answers in edges, nodes, and connection-specific shapes, and letting that structure reach into components means every product page is written against a serialization format instead of a product. Shared queries, mutations, and fragments go through one fetch wrapper, and product and collection responses are reshaped into plain typed objects before a component sees them. The cost was a real integration layer built before the first product rendered. The payoff is that the GraphQL details stay in one place and the pages are written against a domain model.

In front of all of it sat a pre-launch access gate: a code validated in a server action, an HTTP-only cookie holding the result, and a launch countdown whose date lived in Firebase so it could be moved without a deploy. It was scoped as a campaign gate rather than customer authentication, which is why it controls visibility and nothing else.

Stack

Subscription product — Next.js · TypeScript · Tailwind CSS · GSAP

Backend and data — Node · Express on Firebase Cloud Functions · Firebase Authentication, data, and storage

Billing and lifecycle — Stripe · RevenueCat · Klaviyo

Merch storefront — Next.js 15 App Router · TypeScript · Shopify Storefront GraphQL · Zustand · React Query

Engineering challenges

What the cart is allowed to remember

The problem. A merch catalog is products with color and size options, but what a customer actually buys is one specific variant. A cart that keys on the product collapses a medium black tee and a large white one into a single line, and the count in the drawer stops describing what the customer selected.

Why it was hard. The cart persists in the browser, so whatever identity it stores becomes the identity everything downstream inherits: quantity controls, the drawer, the full cart page, and the checkout handoff. Getting it wrong is quiet — the cart looks like it works right up until the wrong thing is carried into checkout. The other half of the problem pulls in the opposite direction. A persisted cart that also stores the product snapshot will happily show whatever was true when the item was added: last month's price, an image that changed, an item that has since sold out.

The decision. The variant is the line item, and the cart remembers identity only. The browser stores what you chose; Shopify answers what it costs and whether it is still available.

Implementation. A Zustand store persisted to local storage holds the Shopify variant ID, the selected color and size, quantity, and just enough product identity to render a reference — so two variants of the same product stay separate lines by construction. React Query then hydrates those references against Shopify, and price, imagery, and availability are read from that response rather than from storage. The drawer, the full cart page, quantity controls, mobile behavior, and empty states all render from that one representation. The accepted tradeoff is that the cart is local to the browser rather than following a customer across devices, and hydration has to stay keyed tightly to what is actually in the cart.

Outcome. Variants stay distinct from selection through to checkout, and the cart survives a reload without also preserving what the price used to be — with no cart infrastructure to run on the server.

Options you can select but cannot buy

The problem. Defaulting a product page to the first listed color and size means it can open on a combination that is not purchasable, with every swatch looking equally selectable.

Why it was hard. Availability is a property of the variant, not of the option. A color can be in stock in one size and gone in another, so the UI has to reflect a cross-product of options against real variant availability rather than treating color and size as independent lists. The first-listed option is just an ordering artifact of the catalog, and defaulting to it is a guess that gets less accurate as inventory moves.

The decision. Let inventory drive selection instead of catalog order.

Implementation. Variant availability helpers derive selectable state from Shopify's per-variant availability data. Initial selection lands on the first option that is actually available rather than the first one listed, and unavailable values are rendered as disabled rather than being selectable and failing later.

Outcome. A product page opens on something that can be added to the cart, and combinations that cannot be bought are visible as unavailable in the option UI rather than at the end of the flow.

Deciding what the storefront would never own

The problem. At some point a cart has to become a payment, and that is the decision that determines how much of a commerce system you have signed up to build.

Why it was hard. Running checkout inside the storefront means server-side cart and checkout infrastructure, session identity the application otherwise did not need, and ownership of address, tax, shipping, and payment flows. For a branded merch storefront, that is a large surface where almost none of the differentiation lives — and Shopify already runs all of it for the rest of the business.

The decision. Keep payment out of the storefront entirely and hand off to Shopify-hosted checkout.

Implementation. The cart builds a Shopify cart permalink from what it already holds: each line's global variant ID is reduced to the numeric identifier the cart URL expects, combined with its quantity, and assembled into a single multi-item URL. The customer moves from the local cart straight into Shopify's own checkout, and no payment interface exists in the storefront to maintain.

Outcome. The storefront ends at the boundary where the value it adds ends. Branding, browsing, variant selection, and the cart are its own; payment, tax, shipping, and order creation stay where the business already handles them.

Outcome

On the subscription product, the delivered work covers the path a member actually walks — landing page, subscription checkout, and a dashboard where the membership, payment method, and profile are managed — with the admin panel, affiliate tracking, analytics, and lifecycle integrations operating against the same subscription state behind it.

The storefront reaches a complete browse-to-checkout path on top of a catalog it does not own: collection and product pages with color and size selection driven by live availability, a persistent variant-level cart with a drawer and a full cart page, and a handoff into Shopify-hosted checkout.

What I would point to across both is where the boundaries were drawn. The storefront owns the experience and the exact identity of a line item, and nothing about money. The subscription product owns the membership relationship and the systems that depend on knowing it. Each one stops where a system that already does the job better begins.