Shopify Metafields & Tags Reference
Appstle Memberships uses Shopify metafields and tags to store membership data, control access via customer tags, and enable storefront gating logic. This reference covers every metafield and tag — what it contains, when it's set, and how to use it in your integration.
All metafields use the namespace appstle_membership and are managed through the Shopify GraphQL Admin API (MetafieldsSetMutation).
Metafields are set on three Shopify resource types:
| Resource | Count | Visibility | Description |
|---|---|---|---|
| Shop | 6 keys | Public (readable by any app/theme) | Membership settings, selling plans, access rules, checkout validation, widget labels |
| Customer | 2 keys | Public | Membership contracts, trial/dunning state |
| Order | 1 key | Public | Subscription context for each order |
Namespace visibility: All membership metafields use the
appstle_membershipnamespace (no$app:prefix), which means they are readable by other apps, themes, and Liquid templates.
Shop metafields store the membership program configuration and are used by the storefront for access gating, checkout validation, and widget rendering.
When updated: On every settings save in the Appstle admin. Updates are synchronous (immediate).
| Property | Value |
|---|---|
| Type | json |
| Resource | Shop |
| Purpose | Shop-level membership settings snapshot for storefront access |
| Set by | SubscribeItScriptUtils.updateShopMetafieldsForSettings() |
| Property | Value |
|---|---|
| Type | json |
| Resource | Shop |
| Purpose | All selling plans configured for this shop's membership program |
| Set by | SubscribeItScriptUtils.updateShopMetafieldsForSettings() |
[
{
"id": "gid://shopify/SellingPlan/111",
"name": "Basic Monthly Membership",
"billingPolicy": {
"interval": "MONTH",
"intervalCount": 1
},
"customerTag": "basic-member",
"orderTag": "membership-order"
},
{
"id": "gid://shopify/SellingPlan/222",
"name": "Premium Annual Membership",
"billingPolicy": {
"interval": "YEAR",
"intervalCount": 1
},
"customerTag": "premium-member",
"orderTag": "premium-membership-order"
}
]| Property | Value |
|---|---|
| Type | json |
| Resource | Shop |
| Purpose | Rules keyed by customer tag — used for storefront gating logic (controlling which products, collections, or pages are accessible to which membership tiers) |
| Set by | SubscribeItScriptUtils.updateShopMetafieldsForSettings() |
{
"basic-member": {
"accessibleCollections": ["gid://shopify/Collection/111"],
"accessibleProducts": [],
"gatingType": "COLLECTION"
},
"premium-member": {
"accessibleCollections": ["gid://shopify/Collection/111", "gid://shopify/Collection/222"],
"accessibleProducts": ["gid://shopify/Product/333"],
"gatingType": "COLLECTION_AND_PRODUCT"
}
}How gating works: The storefront reads this metafield and the customer's tags to determine what content is visible. If a customer has the
premium-membertag, they see all products and collections mapped to that tag. Non-members or lower tiers see gated content as locked/hidden.
| Property | Value |
|---|---|
| Type | json |
| Resource | Shop |
| Purpose | Checkout validation configuration — enforces member-only product purchase rules at checkout |
| Set by | SubscribeItScriptUtils.updateShopMetafieldsForSettings() |
| Property | Value |
|---|---|
| Type | json |
| Resource | Shop |
| Purpose | Storefront widget label translations (default locale) |
| Set by | LabelTranslationsServiceImpl.syncTranslationToShopify() |
| Property | Value |
|---|---|
| Type | json |
| Resource | Shop |
| Purpose | Customer portal label translations (default locale) |
| Set by | LabelTranslationsServiceImpl.syncTranslationToShopify() |
Multi-language support: For non-default locales, labels are NOT stored as separate metafields. Instead, the app fetches the existing default-locale metafield ID and registers a Shopify Translation on it (using
TranslationsRegisterMutation). This leverages Shopify's built-in translation system.
| Property | Value |
|---|---|
| Type | json |
| Resource | Order |
| Purpose | Full membership contract context for this order |
| Set by | MiscellaneousResource.updateOrderMetafields() (membership) and AbstractSubscriptionService.updateOrderMetafieldsFromQueue() (membership-async) |
| Updated when | Order is created via membership (initial purchase or recurring billing) |
| Update mechanism | Queued via SQS (membership-update-order-metafields.fifo), processed asynchronously |
This metafield contains a complete snapshot of the membership context at the time the order was created:
{
"customer": {
"id": "gid://shopify/Customer/1234567890",
"name": "Jane Smith",
"email": "jane@example.com"
},
"subscriptionContract": {
"id": "gid://shopify/SubscriptionContract/9876543210",
"status": "ACTIVE",
"sellingPlanIds": ["gid://shopify/SellingPlan/111"],
"sellingPlanNames": ["Premium Monthly Membership"],
"variantIds": ["gid://shopify/ProductVariant/222"],
"variantNames": ["Premium Membership"]
},
"firstOrder": {
"id": "gid://shopify/Order/444",
"createdAt": "2025-01-15T10:30:00Z"
}
}| Property | Value |
|---|---|
| Type | json |
| Resource | Customer |
| Purpose | All membership contracts for this customer with full details |
| Set by | SubscriptionContractDetailsServiceImpl.maybeUpdateCustomerMetaFields() (membership) and AbstractSubscriptionService.maybeUpdateCustomerMetaFieldsFromQueue() (membership-async) |
| Updated when | Any membership contract changes (created, updated, paused, cancelled, billing attempt) |
| Update mechanism | Queued via SQS (membership-update-customer-metafields.fifo), processed asynchronously |
[
{
"id": "gid://shopify/SubscriptionContract/9876543210",
"status": "ACTIVE",
"sellingPlanIds": ["gid://shopify/SellingPlan/111"],
"sellingPlanNames": ["Premium Monthly Membership"],
"variantIds": ["gid://shopify/ProductVariant/222"],
"variantNames": ["Premium Membership"],
"nextBillingDate": "2025-04-15T10:30:00Z"
}
]| Property | Value |
|---|---|
| Type | json |
| Resource | Customer |
| Purpose | Customer-level membership settings: tracks trial tags and dunning tags for this customer's active plans |
| Set by | Same as subscriptions metafield (updated together) |
| Updated when | Membership state changes (trial start/end, billing failure/success) |
{
"trialTags": "basic-member,premium-member",
"dunningTags": "premium-member"
}| Field | Purpose |
|---|---|
trialTags | Comma-separated list of plan customer tags where the customer is currently in a free trial period (no successful billing yet, within trial window) |
dunningTags | Comma-separated list of plan customer tags where the customer has a recent failed billing attempt requiring retry |
Note: The same key
settingis used for both Shop-level and Customer-level metafields, but they contain different data structures. The resource type (Shop vs Customer) distinguishes them.
Appstle Memberships applies tags to both Customer and Order resources. Customer tags are the primary mechanism for membership access control — they determine which content, products, and collections a member can access.
All tags are applied using the Shopify GraphQL Admin API (TagsAddMutation / TagsRemoveMutation).
This is the core tag mechanism for Appstle Memberships. Each selling plan (membership tier) has a merchant-configured customerTag that controls access.
| Property | Value |
|---|---|
| Resource | Customer |
| Format | Free-form string configured per plan (e.g., basic-member, premium-member, vip-member) |
| Configuration | Set per selling plan in the Appstle admin under each membership plan's settings |
| Source field | SubscriptionGroupPlan.customerTag |
| Event | Tag Action |
|---|---|
| Membership contract ACTIVE | Plan's customerTag is added to the customer |
| Membership contract CANCELLED | Plan's customerTag is removed (immediately if immediateTagRemoveOnCancel=true, otherwise at nextBillingDate) |
| Membership contract PAUSED | Plan's customerTag is removed (immediately if immediateTagRemoveOnPause=true, otherwise at nextBillingDate) |
| Membership contract in DUNNING (failed payment) | Plan's customerTag is removed |
| Membership contract RESUMED | Plan's customerTag is re-added |
Merchants can configure when tags are removed on cancellation or pause:
| Setting | Default | Behavior |
|---|---|---|
immediateTagRemoveOnCancel | false | If false, tag stays until nextBillingDate (member keeps access until their paid period ends). If true, tag is removed immediately. |
immediateTagRemoveOnPause | false | Same behavior for paused memberships. |
Best practice: Keep the defaults (
false) to ensure members retain access for the period they've already paid for. Only set totrueif your membership grants access to ongoing services (not prepaid periods).
When removing a customer tag on cancel/pause, the app checks all other active memberships for the same customer. If another active contract uses the same customerTag, the tag is not removed.
Method: getCustomerTagsForOtherActiveMembership() — ensures a customer who holds the same membership tag from two different contracts doesn't lose access when one is cancelled.
A customer has two contracts:
- "Basic Monthly" →
customerTag: basic-member(ACTIVE) - "Basic Annual" →
customerTag: basic-member(ACTIVE)
If the customer cancels "Basic Monthly", the basic-member tag is not removed because "Basic Annual" is still active.
| Property | Value |
|---|---|
| Resource | Customer |
| Format | Same customerTag as the plan — no separate trial tag |
| Active during | Free trial period (no successful billing yet, within freeTrialCount + freeTrialInterval window) |
During a free trial:
- The plan's
customerTagis applied (member gets access during trial) - The tag is tracked in the customer's
settingmetafield undertrialTags - When the trial ends (expires without payment), the tag is removed
Key point: Trial members get the same tag as paying members. There is no separate "trial" tag — access is identical. The
trialTagsfield in the customer metafield is for internal tracking only.
| Property | Value |
|---|---|
| Resource | Customer |
| Format | Same customerTag as the plan |
| Active during | Period when a billing attempt has failed and retries are pending |
When a billing attempt fails:
- The plan's
customerTagmay be removed (member loses access during dunning) - The tag is tracked in the customer's
settingmetafield underdunningTags - If payment succeeds on retry, the tag is re-added
When a member changes plans (variant swap), tags are swapped in a single operation:
| Step | Action |
|---|---|
| 1 | Old plan's customerTag is removed |
| 2 | New plan's customerTag is added |
| 3 | If the upgrade requires payment and that payment fails, rollback: new tag removed, old tag restored |
Rollback method: SubscriptionBillingAttemptService.rollbackCustomerTags() (membership-async)
| Property | Value |
|---|---|
| Resource | Order |
| Format | Free-form string configured per plan |
| Configuration | Set per selling plan in the Appstle admin |
| Source field | SubscriptionGroupPlan.orderTag |
| Applied when | On initial membership purchase and on each renewal billing (unless skipRecurringOrderTag is enabled) |
| Removed | Never (order tags are permanent) |
| Property | Value |
|---|---|
| Resource | Order |
| Config field | ShopInfo.firstTimeOrderTag |
| Format | Liquid template string |
| Applied when | Initial membership contract creation only (not on renewals) |
| Condition | Only applied if firstTimeOrderTag is non-empty AND the event is SUBSCRIPTION_CONTRACT_CREATE |
| Removed | Never |
| Property | Value |
|---|---|
| Resource | Order |
| Config field | ShopInfo.recurringOrderTag |
| Format | Liquid template string |
| Applied when | Each recurring billing attempt that creates an order |
| Removed | Never |
Both firstTimeOrderTag and recurringOrderTag support Liquid template syntax using the TagModel:
| Variable | Type | Description | Example |
|---|---|---|---|
{{customer.id}} | String | Shopify customer GID | gid://shopify/Customer/1234567890 |
{{subscriptionContract.id}} | String | Subscription contract GID | gid://shopify/SubscriptionContract/9876 |
{{firstOrder.id}} | String | First order GID | gid://shopify/Order/444 |
{{firstOrder.createdAt}} | String | First order creation date (ISO 8601) | 2025-01-15T10:30:00Z |
Static tag:
membership_first_orderDynamic tag with contract info:
membership_{{subscriptionContract.id}}Result: membership_gid://shopify/SubscriptionContract/9876
| Property | Value |
|---|---|
| Resource | Order |
| Applied when | A plan upgrade/downgrade (variant swap) is processed |
| Tag value | New plan's orderTag value |
| Set by | VariantSwapPostProcessService (membership-async) and SubscriptionContractDetailsServiceImpl |
When transferOrderNoteAttributesToSubscription is enabled (default: true), the app transfers the original order's customAttributes (note attributes) to each renewal order. These are not tags but Shopify's native key-value pairs on orders.
| Setting | Default | Description |
|---|---|---|
transferOrderNoteAttributesToSubscription | true | Transfer original order's custom attributes to renewal orders |
| Resource | Namespace | Key | Type | Visibility |
|---|---|---|---|---|
| Shop | appstle_membership | setting | json | Public |
| Shop | appstle_membership | all_selling_plans | json | Public |
| Shop | appstle_membership | rules_by_customer_tag | json | Public |
| Shop | appstle_membership | checkout_validation | json | Public |
| Shop | appstle_membership | widget_label | json | Public |
| Shop | appstle_membership | customer_portal_label | json | Public |
| Customer | appstle_membership | subscriptions | json | Public |
| Customer | appstle_membership | setting | json | Public |
| Order | appstle_membership | details | json | Public |
| Resource | Tag Source | When Applied | When Removed | Permanent? |
|---|---|---|---|---|
| Customer | SubscriptionGroupPlan.customerTag | Contract ACTIVE or TRIAL | Contract CANCELLED, PAUSED, DUNNING, or EXPIRED | No |
| Customer | (same tag on upgrade) | Plan upgrade | Plan downgrade rollback | No |
| Order | SubscriptionGroupPlan.orderTag | Order created (first or renewal) | Never | Yes |
| Order | ShopInfo.firstTimeOrderTag (Liquid) | First order only | Never | Yes |
| Order | ShopInfo.recurringOrderTag (Liquid) | Each renewal | Never | Yes |
| Order | New plan's orderTag | Variant swap | Never | Yes |
| Mutation | Purpose |
|---|---|
MetafieldsSetMutation | Create or update metafields on any resource |
TagsAddMutation | Add tags to orders or customers |
TagsRemoveMutation | Remove tags from customers |
TranslationsRegisterMutation | Register translations for label metafields |
Can I read membership metafields from my Liquid theme?
Yes. All membership metafields use the appstle_membership namespace (no $app: prefix), so they are accessible in Liquid templates via {{ shop.metafields.appstle_membership.setting }}, {{ customer.metafields.appstle_membership.subscriptions }}, etc.
How does membership access gating work with tags?
Each membership plan has a customerTag (e.g., premium-member). When a customer's contract is active, this tag is added to their Shopify customer profile. The rules_by_customer_tag shop metafield maps each tag to the collections/products that tag grants access to. Your theme reads these metafields and the customer's tags to show or hide gated content.
What happens when a member cancels mid-billing-cycle?
By default (immediateTagRemoveOnCancel=false), the customer keeps their membership tag until nextBillingDate. This means they retain access for the period they've already paid for. Set immediateTagRemoveOnCancel=true to remove access immediately on cancellation.
What if a customer has the same tag from two different memberships?
The app has cross-membership protection. When removing a tag due to cancellation/pause, it checks if any other active contract for this customer uses the same tag. If so, the tag is not removed. The customer retains access.
How are customer metafields updated — in real-time or batched?
Customer metafield updates are queued via SQS (membership-update-customer-metafields.fifo) and processed asynchronously by membership-async. Typical latency is a few seconds, but during high-traffic periods it may take longer.
Do trial members get the same tag as paying members?
Yes. During a free trial, the plan's customerTag is applied — there is no separate trial tag. Trial members get the same access as paying members. The trialTags field in the customer setting metafield tracks which tags are in trial for internal use only.
What happens during dunning (failed payment)?
When a billing attempt fails, the plan's customerTag may be removed (member loses access). The dunningTags field in the customer setting metafield tracks which tags are in dunning. If a retry succeeds, the tag is re-added and access is restored.