{"slug":"wcm-data-model-subscriptions-link","title":"wcm-data-model-subscriptions-link","summary":"WooCommerce Memberships storage and relationship map for","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:21.135164Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-21T19:53:59Z"},"bodyHtml":"<hr>\n<h2>name: wcm-data-model-subscriptions-link\ndescription: WooCommerce Memberships storage and relationship map for\nmembership plan CPTs, user membership CPTs, post statuses, plan/user\nmembership meta keys, rule storage, profile-field storage, order grant\nmeta, and WooCommerce Subscriptions-linked memberships. Use when code\nreads or writes wc_membership_plan, wc_user_membership, wcm-* statuses,\nwc_memberships_rules, _subscription_id, _has_installment_plan,\n_free_trial_end_date, membership plan access meta, user membership\ndates, product/order grant meta, or when an agent needs exact Memberships\nCPT/meta names and the Memberships-Subscriptions relation.\nmetadata:\nwp-skills-author: \"Soczo Kristof\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"woocommerce-memberships\"\nwp-skills-plugin-version-tested: \"1.29.0\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-07-06\"</h2>\n<h1>WooCommerce Memberships: data model and Subscriptions link</h1>\n<p>Use this when an integration needs exact storage names. Prefer Memberships public APIs and objects for writes; use these keys for audits, migrations, import/export mapping, and low-level debugging.</p>\n<h2>Core storage</h2>\n<table>\n<thead>\n<tr>\n<th>Entity</th>\n<th>Storage</th>\n<th>Name/key</th>\n<th>Notes</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Membership plan</td>\n<td>CPT</td>\n<td><code>wc_membership_plan</code></td>\n<td>Registered by <code>WC_Memberships_Post_Types</code>. Not public, UI under WooCommerce when possible.</td>\n</tr>\n<tr>\n<td>User membership</td>\n<td>CPT</td>\n<td><code>wc_user_membership</code></td>\n<td>One post per user-plan membership. Not public.</td>\n</tr>\n<tr>\n<td>Plan relation</td>\n<td>WP post field</td>\n<td><code>wc_user_membership.post_parent</code></td>\n<td>This is the plan ID. Do not invent <code>_member_plan_id</code>; the installed plugin uses <code>post_parent</code>.</td>\n</tr>\n<tr>\n<td>Member relation</td>\n<td>WP post field</td>\n<td><code>wc_user_membership.post_author</code></td>\n<td>This is the member/user ID.</td>\n</tr>\n<tr>\n<td>Subscription</td>\n<td>WC order type</td>\n<td><code>shop_subscription</code></td>\n<td>From WooCommerce Subscriptions; use <code>wcs_get_subscription()</code>.</td>\n</tr>\n<tr>\n<td>Membership rules</td>\n<td>Option</td>\n<td><code>wc_memberships_rules</code></td>\n<td>Stores serialized rule arrays for all plans. Rules are not CPTs.</td>\n</tr>\n<tr>\n<td>Profile field definitions</td>\n<td>Option</td>\n<td><code>wc_memberships_profile_fields</code></td>\n<td>Definition data keyed by profile field slug.</td>\n</tr>\n<tr>\n<td>Profile field values</td>\n<td>User meta</td>\n<td><code>_wc_memberships_profile_field_{slug}</code></td>\n<td>Stored by <code>Profile_Field\\User_Meta</code>.</td>\n</tr>\n<tr>\n<td>Order grant marker</td>\n<td>WC order meta</td>\n<td><code>_wc_memberships_access_granted</code></td>\n<td>Array keyed by user membership ID, with <code>already_granted</code>, <code>granting_order_status</code>, and related details.</td>\n</tr>\n</tbody>\n</table>\n<h2>Membership statuses</h2>\n<p>WP post statuses use the <code>wcm-</code> prefix. Object methods usually accept/return unprefixed slugs.</p>\n<table>\n<thead>\n<tr>\n<th>WP status</th>\n<th>Object status</th>\n<th>Meaning</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>wcm-active</code></td>\n<td><code>active</code></td>\n<td>Active access.</td>\n</tr>\n<tr>\n<td><code>wcm-delayed</code></td>\n<td><code>delayed</code></td>\n<td>Membership exists but access starts later.</td>\n</tr>\n<tr>\n<td><code>wcm-complimentary</code></td>\n<td><code>complimentary</code></td>\n<td>Admin/complimentary access.</td>\n</tr>\n<tr>\n<td><code>wcm-free_trial</code></td>\n<td><code>free_trial</code></td>\n<td>Added by the Subscriptions integration when a linked subscription is in trial.</td>\n</tr>\n<tr>\n<td><code>wcm-paused</code></td>\n<td><code>paused</code></td>\n<td>Paused membership.</td>\n</tr>\n<tr>\n<td><code>wcm-expired</code></td>\n<td><code>expired</code></td>\n<td>Expired membership.</td>\n</tr>\n<tr>\n<td><code>wcm-cancelled</code></td>\n<td><code>cancelled</code></td>\n<td>Cancelled membership.</td>\n</tr>\n</tbody>\n</table>\n<p>Use <code>wc_memberships_get_user_membership_statuses()</code> for the installed status catalog and <code>wc_memberships()-&gt;get_user_memberships_instance()-&gt;get_active_access_membership_statuses()</code> for statuses that grant access.</p>\n<h2>Membership plan meta</h2>\n<p>These keys are stored on <code>wc_membership_plan</code> posts and exposed by <code>WC_Memberships_Membership_Plan::get_meta_keys()</code>.</p>\n<table>\n<thead>\n<tr>\n<th>Meta key</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>_access_method</code></td>\n<td><code>manual-only</code>, <code>signup</code>, or <code>purchase</code> access method.</td>\n</tr>\n<tr>\n<td><code>_access_length</code></td>\n<td>Access length value, e.g. <code>unlimited</code>, <code>specific</code>, <code>fixed</code>, or an amount/period value depending on method.</td>\n</tr>\n<tr>\n<td><code>_access_start_date</code></td>\n<td>Fixed access start datetime.</td>\n</tr>\n<tr>\n<td><code>_access_end_date</code></td>\n<td>Fixed access end datetime.</td>\n</tr>\n<tr>\n<td><code>_product_ids</code></td>\n<td>Product or variation IDs that grant access.</td>\n</tr>\n<tr>\n<td><code>_members_area_sections</code></td>\n<td>Enabled members-area sections.</td>\n</tr>\n<tr>\n<td><code>_email_content</code></td>\n<td>Plan-specific email content settings.</td>\n</tr>\n</tbody>\n</table>\n<p>Use plan methods such as <code>get_access_method()</code>, <code>set_access_method()</code>, <code>get_product_ids()</code>, <code>set_product_ids()</code>, <code>get_access_start_date()</code>, and <code>get_access_end_date()</code> rather than direct meta writes.</p>\n<h2>User membership meta</h2>\n<p>These keys are stored on <code>wc_user_membership</code> posts and exposed by <code>WC_Memberships_User_Membership::get_meta_keys()</code>.</p>\n<table>\n<thead>\n<tr>\n<th>Meta key</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>_start_date</code></td>\n<td>Membership start datetime.</td>\n</tr>\n<tr>\n<td><code>_end_date</code></td>\n<td>Membership end datetime. Empty/unset can mean unlimited.</td>\n</tr>\n<tr>\n<td><code>_cancelled_date</code></td>\n<td>Cancellation datetime.</td>\n</tr>\n<tr>\n<td><code>_paused_date</code></td>\n<td>Current pause start datetime.</td>\n</tr>\n<tr>\n<td><code>_paused_intervals</code></td>\n<td>Historical paused intervals.</td>\n</tr>\n<tr>\n<td><code>_product_id</code></td>\n<td>Product or variation ID that granted access.</td>\n</tr>\n<tr>\n<td><code>_order_id</code></td>\n<td>Order ID that granted access.</td>\n</tr>\n<tr>\n<td><code>_previous_owners</code></td>\n<td>Previous owner user IDs after transfers.</td>\n</tr>\n<tr>\n<td><code>_renewal_login_token</code></td>\n<td>Auto-login token used for renewal flows.</td>\n</tr>\n<tr>\n<td><code>_locked</code></td>\n<td>Operation lock for race-sensitive flows.</td>\n</tr>\n</tbody>\n</table>\n<p>Use <code>wc_memberships_create_user_membership()</code>, <code>wc_memberships_get_user_membership()</code>, and <code>WC_Memberships_User_Membership</code> methods for writes and lifecycle side effects.</p>\n<h2>Rule storage</h2>\n<p>Rules are stored in the <code>wc_memberships_rules</code> option, not as posts. Rule type values:</p>\n<table>\n<thead>\n<tr>\n<th>Rule type</th>\n<th>Purpose</th>\n<th>Important fields</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>content_restriction</code></td>\n<td>Restricts posts, pages, CPTs, or terms.</td>\n<td><code>content_type</code>, <code>content_type_name</code>, <code>object_ids</code>, <code>access_schedule</code>, <code>access_schedule_exclude_trial</code>.</td>\n</tr>\n<tr>\n<td><code>product_restriction</code></td>\n<td>Restricts product viewing or purchase.</td>\n<td>Same fields plus <code>access_type</code> (<code>view</code> or <code>purchase</code>).</td>\n</tr>\n<tr>\n<td><code>purchasing_discount</code></td>\n<td>Applies member discounts.</td>\n<td><code>discount_type</code>, <code>discount_amount</code>, <code>active</code>.</td>\n</tr>\n</tbody>\n</table>\n<p>Common rule fields are <code>id</code>, <code>membership_plan_id</code>, <code>active</code>, <code>rule_type</code>, <code>content_type</code>, <code>content_type_name</code>, <code>object_ids</code>, <code>discount_type</code>, <code>discount_amount</code>, <code>access_type</code>, <code>access_schedule</code>, <code>access_schedule_exclude_trial</code>, and <code>meta_data</code>.</p>\n<p>Use <code>wc_memberships()-&gt;get_rules_instance()-&gt;get_rules()</code>, <code>get_plan_rules()</code>, <code>add_rules()</code>, <code>update_rules()</code>, or <code>delete_rules()</code> instead of editing the option directly.</p>\n<p>Memberships 1.29.0 adds a safer admin/editor layer for per-post content restriction rules. <code>PostRestrictionRulesSerializer</code> reads the same <code>wc_memberships_rules</code> store and returns both direct and inherited rules, while <code>SetPostRules</code> replaces only direct post-specific <code>content_restriction</code> rows for one post. If you consume the GET result, filter to rows where <code>editable === true</code> before sending an update; inherited rules must be edited at the plan/post-type/taxonomy source.</p>\n<h2>Memberships to Subscriptions relation</h2>\n<p>WooCommerce Memberships links to WooCommerce Subscriptions by adding extra meta to the <code>wc_user_membership</code> post. The subscription itself is still a WCS <code>shop_subscription</code>.</p>\n<table>\n<thead>\n<tr>\n<th>User membership meta</th>\n<th>Purpose</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>_subscription_id</code></td>\n<td>Linked <code>shop_subscription</code> ID. This is the primary relation key from membership to subscription.</td>\n</tr>\n<tr>\n<td><code>_has_installment_plan</code></td>\n<td>Flag/value used when the linked subscription is treated as an installment plan. The code stores the subscription ID when it auto-detects the flag.</td>\n</tr>\n<tr>\n<td><code>_free_trial_end_date</code></td>\n<td>Trial end datetime mirrored from the linked subscription trial.</td>\n</tr>\n</tbody>\n</table>\n<p>Public helpers:</p>\n<pre><code>$integration = wc_memberships()-&gt;get_integrations_instance()-&gt;get_subscriptions_instance();\n$subscription = $integration ? $integration-&gt;get_subscription_from_membership( $user_membership ) : null;\n$memberships = $integration ? $integration-&gt;get_memberships_from_subscription( $subscription ) : array();\n\n$is_linked = wc_memberships_is_user_membership_linked_to_subscription( $user_membership );\n</code></pre>\n<p>Do not assume a reverse meta key on the subscription. Reverse lookup queries <code>wc_user_membership</code> posts with <code>_subscription_id = {subscription_id}</code>.</p>\n<h2>Subscription-linked lifecycle</h2>\n<p>When Subscriptions is active, Memberships may return <code>WC_Memberships_Integration_Subscriptions_User_Membership</code> for linked memberships.</p>\n<p>Important behavior:</p>\n<ul>\n<li>Linking calls <code>WC_Memberships_Integration_Subscriptions_User_Membership::set_subscription_id()</code> and fires <code>wc_memberships_user_membership_linked_to_subscription</code>.</li>\n<li>Unlinking deletes <code>_subscription_id</code> and fires <code>wc_memberships_user_membership_unlinked_from_subscription</code>.</li>\n<li>Subscription trial dates can set membership status to <code>free_trial</code> and write <code>_free_trial_end_date</code>.</li>\n<li>Subscription date/status changes can update linked membership dates/statuses.</li>\n<li>If a plan is granted by subscription products, Memberships checks whether the subscription product grants the plan and may keep access tied to subscription activity.</li>\n<li>Installment plan behavior differs: completion can unlink the subscription while keeping membership access according to the plan rules.</li>\n</ul>\n<h2>Gifting and Memberships</h2>\n<p>WooCommerce Subscriptions Gifting changes who receives membership access when the gifted subscription product grants a Memberships plan.</p>\n<p>Storage bridge:</p>\n<ul>\n<li>Gifted subscription stores recipient user on subscription meta <code>_recipient_user</code> after recipient account resolution.</li>\n<li>Parent order item stores <code>wcsg_recipient = wcsg_recipient_id_{user_id}</code>.</li>\n<li>Memberships grant filtering uses <code>wcsg_recipient</code> to grant access to recipient users instead of the purchaser for gifted items.</li>\n<li>The membership still stores the linked subscription in <code>_subscription_id</code>.</li>\n</ul>\n<p>If an order has the same membership-granting product for several recipients, do not pick the first subscription in the order. Use the WCSG/Memberships integration logic or query recipient subscriptions before assigning <code>_subscription_id</code>.</p>\n<h2>REST and directory data boundaries</h2>\n<p>Memberships REST records are API projections, not storage contracts. The v4 directory endpoint deliberately returns less than the full user membership record:</p>\n<ul>\n<li>It requires a <code>page_id</code> containing a Memberships Directory block and optionally a matching <code>block_instance_id</code>.</li>\n<li>It validates the viewer's access to that page/block context.</li>\n<li>It whitelists response keys to <code>id</code>, <code>customer_data</code>, <code>plan_name</code>, <code>profile_fields</code>, and <code>meta_data</code>.</li>\n<li>It blanks <code>meta_data</code>, filters <code>profile_fields</code> to the block's configured slugs, and removes REST links.</li>\n</ul>\n<p>Do not use <code>/wc/v4/memberships/members/directory</code> as an admin export or integration source. Use the admin-gated Memberships REST endpoints, public PHP APIs, or the Abilities API for privileged automation.</p>\n<h2>Safe query patterns</h2>\n<pre><code>// Memberships for a subscription.\n$memberships = get_posts( array(\n    'post_type'      =&gt; 'wc_user_membership',\n    'post_status'    =&gt; wc_memberships_get_user_membership_statuses( false ),\n    'fields'         =&gt; 'ids',\n    'posts_per_page' =&gt; -1,\n    'meta_query'     =&gt; array(\n        array(\n            'key'   =&gt; '_subscription_id',\n            'value' =&gt; $subscription_id,\n        ),\n    ),\n) );\n\n// Prefer API for normal code.\n$user_membership = wc_memberships_get_user_membership( $user_id, $plan_id );\n</code></pre>\n<p>For \"grant access from previous purchase\" logic, do not query <code>shop_order</code> posts directly. WooCommerce order storage may be HPOS, while Memberships still stores plans/user memberships as CPTs. Use Memberships grant/order APIs and WooCommerce order APIs so previous-purchase checks work in both CPT and HPOS order storage.</p>\n<h2>Common mistakes</h2>\n<pre><code>// WRONG: this key is not the plan relation in the installed plugin.\n$plan_id = get_post_meta( $membership_id, '_member_plan_id', true );\n\n// RIGHT: use the object or post_parent.\n$membership = wc_memberships_get_user_membership( $membership_id );\n$plan_id = $membership ? $membership-&gt;get_plan_id() : 0;\n\n// WRONG: raw post status write misses Memberships side effects.\nwp_update_post( array( 'ID' =&gt; $membership_id, 'post_status' =&gt; 'wcm-active' ) );\n\n// RIGHT: use object status API with unprefixed status.\n$membership-&gt;update_status( 'active' );\n</code></pre>\n<h2>Cross-references</h2>\n<ul>\n<li>Use <code>wcm-membership-hooks</code> for lifecycle hooks and REST/webhook extension points.</li>\n<li>Use <code>wcm-access-discounts</code> for access checks, restrictions, drip timing, and member discounts.</li>\n<li>Use <code>wcm-abilities-api</code> for Memberships 1.28+ WP Abilities API automation.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li>Verified source paths:\n<ul>\n<li><code>wp-content/plugins/woocommerce-memberships/src/class-wc-memberships-post-types.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/class-wc-memberships-membership-plan.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/class-wc-memberships-user-membership.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/class-wc-memberships-rules.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/functions/wc-memberships-functions-orders.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Data_Stores/Profile_Field/User_Meta.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Data_Stores/Profile_Field_Definition/Option.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/API/Controller/User_Memberships.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Helpers/Directory_Block_Validator.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Posts/Actions/SetPostRules.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/Posts/Adapters/JsonSerializers/PostRestrictionRulesSerializer.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/integrations/subscriptions/class-wc-memberships-integration-subscriptions-user-membership.php</code></li>\n<li><code>wp-content/plugins/woocommerce-memberships/src/integrations/subscriptions/class-wc-memberships-integration-subscriptions.php</code></li>\n</ul>\n</li>\n</ul>\n","files":[{"path":"SKILL.md","sizeBytes":13464,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:58:36.289977Z","sha256":"2B8328F6CE17C617C96111C6137127FB6DC4FD42D189441AC8B3DF79E832F6E4","sizeBytes":4488},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"woocommerce/wcm-data-model-subscriptions-link","license":"MIT","commit":"8820ff3c301066297e696611e3bc4ebeb47d1851","subtreeSha":"94872C541731ADC442955AF9ED7666797DB4CAB716A5FF59ECA3CE9B802B2EEE","lastSyncedAt":"2026-09-22T13:51:11.366991Z"},"reviewedAt":"2026-09-16T15:19:08.976388Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wcm-data-model-subscriptions-link"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}