Shopify Builds>Three Ships>Swell VIP tier and email-subscriber segmentation resolved in Liquid

Swell VIP Tier and Email-Subscriber Segmentation Resolved in Liquid

Two Liquid variables in theme.liquid: loyalty_status, resolved from the tags the loyalty vendor writes onto the customer record, and subscriber_status, resolved from email-marketing consent — both assigned before content_for_header and emitted only for signed-in customers.

Shopify's storefront JavaScript has no authenticated customer, so a shopper's loyalty tier normally arrives from the loyalty vendor's script on every page. Three Ships' loyalty vendor writes tier membership onto the customer record as tags, so we resolve the tier in Liquid in the layout, alongside email-marketing consent, and hand both to the tag manager. Read at render time, emitted only for signed-in customers, used by nothing on the page.

The Problem

Shopify gives the theme an authenticated customer object at render time and gives the browser nothing equivalent. Any script that wants to know who the shopper is — their tier, their consent, whether they're signed in — has to be told by the page or has to ask a third party. The usual answer is the loyalty vendor's own storefront script. The other place the value exists is the customer record, which Liquid reads at render time.

Three Ships runs a tiered loyalty programme through Swell, and wanted tier and email-subscriber state attached to every analytics hit, so campaigns could be measured and segmented by tier. The page needed to supply that state itself, from something it already had.

The Constraint

Swell writes tier membership back to the Shopify customer record as customer tags, and tags are something Liquid reads for free. That was the only authenticated view of loyalty state the storefront had: no customer metafield exposed it, and the theme carries no Storefront API token that could ask for it. The tier had to be derived from the tag list or not at all.

The customer object exists only on signed-in requests, so anything derived from it has to tolerate its own absence. And the destination was a shared tag manager container, where a guest arriving with a placeholder value looks the same as a member — so the customer keys had to be absent for guests, not empty.

Three Ships also tags customers heavily for segmentation, which means the loop over customer.tags walks a long list on many accounts. It had to stop as soon as it had an answer.

What We Built

Roughly 25 lines of Liquid in theme.liquid, assigned before {{ content_for_header }}.

subscriber_status is assigned 'none', then inside {% if customer %} becomes 'email' when customer.accepts_marketing is true. It reads email-marketing consent and nothing else — not SMS consent, not the subscription app's records. A shopper who subscribes to a product but declines marketing email is 'none' here, and that's correct for what the key means.

loyalty_status is assigned 'non member', then resolved by a {% for tag in customer.tags %} loop with an explicit {% break %} on the first match. Each tier tag is mapped onto the label the programme uses, so the value that leaves the theme is the tier name rather than the tag it was read from. A customer whose tags carry no tier falls through the loop and keeps the default.

Both variables are then interpolated into the page-context push that the layout makes into window.dataLayerloyalty_status under its own name and subscriber_status under the key subscriber — alongside login_status: 'logged-in', inside an {% if customer %} guard. A guest's push simply lacks the three keys. The push itself, its idle timing and the queue it lands in belong to a separate build on this storefront; this page is about where the two values come from and what they can and can't see.

What they can see: the tags and the consent flag as they stood on the customer record when Shopify rendered the page. What they can't: a tier change the vendor makes after render, a points balance, any state the shopper hasn't signed in to reveal. And what nothing on the page does with them: no section reads loyalty_status, no banner switches on it, no cookie or localStorage copy is kept. The values are seeded into the data layer and consumed there. Personalization of the page by tier would be a different build, and it isn't this one.

Why This Way

The tags are already on the customer record and Liquid reads them at no cost, so deriving the tier server-side keeps a value that only ever changes between sessions on the server side too. The value is on the customer record when Shopify renders the page, so the theme reads it there and the page arrives knowing.

The for/break loop rather than a chain of contains tests makes the first matching tag authoritative and exits early on long tag lists. Mapping to the programme's own labels inside the theme keeps the analytics schema in the programme's vocabulary rather than the tag's.

The trade is that the values are render-time truth and nothing more. A tier that changes mid-session isn't reflected until the next request, which is acceptable for a value that moves on order events, but it's a limit we accepted rather than a feature. A guest gets nothing, by design. And because the values live only in the data layer, the theme itself can't act on them — a merchandiser who wants a member-only banner is asking for new code, not a setting.

Why Not an App

The usual way to put a tier on the page is the loyalty vendor's storefront script, which is also the programme's own UI. Here the value the analytics needed was already on the customer record, so the theme reads it there: Swell's widget goes on running the programme's storefront UI, and only the tier read-out moved into the theme, where it costs a loop over a tag list at render time. There's nothing to buy for this; the platform primitive is the customer tag, and the vendor already writes it. What an app would add is a live view of points and pending tier changes, which the analytics use here never needed.

Implementation Notes

  • Both variables are assigned before {{ content_for_header }}, so they're available to any inline script later in the document, not only to the data-layer push.
  • The Liquid variable is subscriber_status; the key it lands under in the push is subscriber. loyalty_status keeps its name across the boundary.
  • {% break %} inside the customer.tags loop makes the first matching tier authoritative if a customer somehow carries two tier tags.
  • Guests are excluded at the point of use — the {% if customer %} guard around the data-layer keys — rather than by emitting empty strings that a trigger could mistake for a state.
  • The values feed the tag manager only; no cookie or localStorage copy is kept, so a tier can never go stale in the browser between sessions.
  • subscriber_status reads customer.accepts_marketing, which is email consent; SMS consent and product-subscription status are not part of it.
  • The emitted label is the programme's tier name, so the analytics schema is written in the programme's vocabulary rather than in the tag's.

Edge Cases

  • Logged-out visitors: both variables are computed with their defaults but never emitted, because the guard is at the push.
  • Logged-in customers whose tags carry no tier fall through the loop to 'non member'.
  • A customer who accepts marketing but holds no tier gets subscriber_status = 'email' and loyalty_status = 'non member' independently; the two axes are never coupled.
  • Tier tags added by Swell after the page rendered aren't reflected until the next request; a shopper promoted mid-session reports the old tier until they navigate.

Platform Primitives Used

  • Customer accounts — the Liquid customer object is the only authenticated source of tags and consent, and it exists only on signed-in requests.
  • dataLayer — the sole consumer of both values: page-scoped keys the container's triggers read, pushed once per page.

Integrations in Play

  • Swell — the loyalty programme, whose tier tags on the customer record are the input. Its widget continues to run the programme's storefront UI; this build reads what it writes and coexists with it.
  • Google Tag Manager — the destination. What any tag does with loyalty_status and subscriber is configured in the container and isn't described here.

Where It Runs

On every page of the storefront: the variables are assigned in the layout, so every template computes them, and every template's data-layer push emits them for a signed-in customer. There's no template list because there's no template where the layout isn't rendered.

What This Demonstrates

  • Subscription purchase options on the product page — the capability this build is indexed under, alongside the storefront's other subscriber-state work. That hub is explicit about which of its builds are purchase controls and which sit adjacent to them; this one is adjacent.
  • Loyalty programme storefront surfaces — the capability this build most directly belongs to: VIP tier resolved from customer tags in Liquid, with no API call.

How We Know

One layout file read from the theme, plus two items in the client's task register covering the storefront's customer-state keys in the data layer. The register establishes that the keys were asked for; the layout establishes what supplies them. What the container does with the values afterwards isn't in the record, and we make no claim about it.

Related Builds

The Buy-vs-Build Question

The programme is bought — Swell holds the points, the tiers and the rules. What the theme owns is knowing, at render time, which tier the shopper is in, and it owns that because the vendor writes a tag. Where the line sits between a rented loyalty engine and the storefront half you build around it is the subject of Points program: buy or build?

Provenance & Evidence

  • Client: Three Ships — threeshipsbeauty.ca
  • Surface: Global — the layout wraps every template
  • Templates served: all of them; the variables are assigned in the layout
  • Complexity: Low — roughly 25 lines of Liquid
  • Attribution: Deploi-authored. The two variable blocks and their place in the data-layer push are ours. They sit in the layout of Palo Alto 5.8.0 by Presidio Creative, a paid premium theme the brand licenses; the theme around them is the vendor's, and the tier tags themselves are written by Swell.
  • Status: Live, verified 2026-09-07
  • Evidence: One layout file read directly, plus two client task-register items
  • Confidence: Strong — the code is unambiguous and the client's task register carries the work
  • Primary capability: Subscription purchase options on the product page

Ready to Tell Your Tags Which Tier a Shopper Is In?

If your container learns loyalty state from a vendor script, or doesn't learn it at all, the customer record may already hold the answer. Contact us today and we'll find out what your theme knows about a signed-in shopper before a single script runs.

More builds