How to Build a Custom "Membership" or "Exclusive Club" Module in Magento 2

How to Build a Custom "Membership" or "Exclusive Club" Module in Magento 2
Want to add a membership or exclusive club feature to your Magento 2 store? Great — you’re in the right place. I’ll walk you through a practical, hands-on approach to building a custom membership module: architecture, roles & permissions, customer integration, pricing/catalog customizations, access restriction, and best practices for making the module maintainable and extensible. I’ll include real code snippets you can copy, tweak, and test on your dev environment.
What this post covers (quick)
- Technical architecture and file layout for a membership module.
- How roles and permissions (ACL) fit into admin management.
- Integration choices with Magento customers and customer groups.
- Techniques to customize prices and catalogs per membership status.
- Methods to secure and restrict access to exclusive content.
- Best practices to keep code extensible and maintainable.
Why build a custom membership module instead of buying one?
Buying an extension can be fast, but a custom module gives precise control over business logic, data model, and integrations (payment flow, hosting, analytics). If you host on magefine.com or sell extensions, building a clean custom module becomes a long-term advantage: you control upgrades, data, and the UX. That said, keep security, performance, and maintainability in mind — they’re crucial for membership features.
High-level architecture
Here’s a compact view of what the module will include and how pieces communicate:
- Module namespace: Magefine_Membership (feel free to namespace differently)
- Database: declarative schema (db_schema.xml) for memberships and membership_user relations
- Models + ResourceModels + Repositories following service contracts
- Admin UI: ACL, adminhtml UI components to manage membership plans
- Checkout / Sales integration: grant membership on successful order / invoice
- Customer integration: customer attribute or customer group mapping
- Pricing/catalog customizations: plugin or observer to adjust price and category visibility
- Content restriction: controller/blocks/plugins to check membership before rendering
Module skeleton
Create the basic registration and module declaration files:
// app/code/Magefine/Membership/registration.php
After that run setup:upgrade to register the module.
Declarative schema: create membership tables
Using declarative schema makes upgrades predictable. We'll add a simple memberships table and a relation table for customer memberships (allows multiple memberships in future).
// app/code/Magefine/Membership/etc/db_schema.xml
Run bin/magento setup:upgrade. This creates tables cleanly and supports rollbacks on future changes.
Models, ResourceModels and Repositories (brief)
Follow Magento best practices: define an interface for the repository, implement model and resource model. Example for Membership model signature:
// app/code/Magefine/Membership/Api/Data/MembershipInterface.php
_init(ResourceModel::class);
}
public function getId(){ return $this->getData('membership_id'); }
public function setId($id){ return $this->setData('membership_id',$id); }
public function getName(){ return $this->getData('name'); }
public function setName($name){ return $this->setData('name',$name); }
public function getPrice(){ return $this->getData('price'); }
public function setPrice($price){ return $this->setData('price',$price); }
}
I won't paste the full ResourceModel and Repository here, but use the standard patterns. Use the generator and code reading from core modules as a template (for example, Sales or Catalog module repositories).
Admin ACL and permissions
Admin users must be able to create and manage membership plans. Add an ACL resource and admin menu items. This allows role-based permissions to be controlled via System > Permissions > User Roles.
// app/code/Magefine/Membership/etc/acl.xml
And add an admin menu (menu.xml) and adminhtml UI components for CRUD. The ACL ids above are used to restrict controllers and blocks.
Integrating with customer system and groups
You have two primary strategies to represent membership on the customer side:
- Customer attribute(s) or a relation table (we already created magefine_membership_customer). Use this to hold membership meta and expiry dates. This keeps membership decoupled from standard customer groups.
- Map membership to customer groups. When membership is purchased, assign the customer to a special customer group so existing Magento group-based price and catalog permissions apply automatically.
Both approaches have pros and cons. Using Magento customer groups leverages built-in pricing & group price functionality but changes a customer’s group (which may interfere with store logic if groups are used for other purposes). Storing membership separately gives more granularity and flexibility.
Example: assign a customer group when membership is activated
Here’s a lightweight observer that runs on sales_order_invoice_pay event (triggered when invoice is paid) to grant membership and set a group. This simplifies price and catalog access because you can use native Customer Group pricing and category visibility.
// app/code/Magefine/Membership/etc/events.xml (adminhtml or global)
// app/code/Magefine/Membership/Observer/GrantMembership.php
customerRepository = $customerRepository;
$this->customerFactory = $customerFactory;
}
public function execute(\Magento\Framework\Event\Observer $observer)
{
$invoice = $observer->getEvent()->getInvoice();
$order = $invoice->getOrder();
foreach($order->getAllItems() as $item){
// assume membership products are simple products with sku prefix MF-MEM-
if(strpos($item->getSku(), 'MF-MEM-') === 0){
$customerId = $order->getCustomerId();
if(!$customerId) continue; // guest
$customer = $this->customerRepository->getById($customerId);
// map to a group id you created for membership, e.g. 4
$customer->setGroupId(4);
$this->customerRepository->save($customer);
// create a record in magefine_membership_customer (left as exercise)
}
}
}
}
That snippet is simplified — real implementation should determine which membership plan the product corresponds to, set start/end dates, and store a record in the membership relation table.
Custom pricing per membership
You have multiple ways to change prices for members:
- Use Magento customer groups with Catalog Price Rules or Group Price. This is the cleanest if membership-to-group mapping works in your business model.
- Plugin on price calculation (FinalPrice or Product model) to adjust price at runtime based on a customer’s membership record.
- Use custom price attributes or tier pricing combined with customer context.
Example: plugin on getFinalPrice
Below is a simple plugin that adjusts the product price when the logged-in customer has an active membership. This example demonstrates the principle but should be optimized for performance (cache checks, avoiding heavy DB calls on every product render).
// app/code/Magefine/Membership/etc/di.xml
// app/code/Magefine/Membership/Plugin/ProductPricePlugin.php
customerSession = $customerSession;
$this->membershipRepo = $membershipRepo;
}
public function afterGetFinalPrice($subject, $result)
{
// $result is the original final price
if(!$this->customerSession->isLoggedIn()) return $result;
$customerId = $this->customerSession->getCustomerId();
// Check membership existence and validity (cache this in session ideally)
$membership = $this->membershipRepo->getActiveMembershipForCustomer($customerId);
if(!$membership) return $result;
// Example: 10% off for members
$discountPercent = 10;
$newPrice = $result * (1 - $discountPercent / 100);
return $newPrice;
}
}
Notes:
- Use session caching or a small in-memory cache to avoid DB calls on each product rendering.
- Consider frontend price render cache keys — if prices differ per customer, ensure blocks are not cached globally for anonymous/other users.
- Better approach: set the customer to a membership-specific customer group and use Magento-native Group Price; that avoids plugins and respects caching.
Restricting access to products, categories and pages
Restricting access has two dimensions: catalog visibility and content pages (CMS) or custom controller endpoints.
Catalog visibility
Options:
- Use category/product attributes to mark them as "members-only" and filter them out from product collections for non-members via plugin or collection extension.
- Use customer group visibility (if you map membership to a group) and configure products/categories per group via catalog permissions available in Adobe Commerce. For Open Source, you might need custom logic.
Example: add product attribute is_members_only and then filter product collections in a plugin on the collection load.
// app/code/Magefine/Membership/Observer/FilterProductCollection.php
// This observer runs when catalog_product_collection_load_before or use a plugin on \Magento\Catalog\Model\ResourceModel\Product\Collection::load
// Pseudo-code: if customer is not member, add ->addAttributeToFilter('is_members_only', ['neq' => 1]);
Controller and CMS page restriction
For custom pages or controllers (for example, a members-only blog, PDF downloads, or special pages), check membership at controller dispatch. Use a plugin for the ActionInterface::dispatch or check inside your controller's execute method.
// app/code/Magefine/Membership/Controller/Exclusive/Download.php
public function execute()
{
$customerId = $this->customerSession->getCustomerId();
if(!$customerId || !$this->membershipRepo->isActiveForCustomer($customerId)){
// redirect to membership upsell page or 403
return $this->resultRedirectFactory->create()->setPath('membership/upsell');
}
// proceed to serve file
}
For security, always protect the file endpoints at server level or use PHP streaming with auth checks. Don’t rely on obfuscated links alone.
Security practices for membership modules
- Never trust client-side checks alone. Always verify membership server-side before serving protected content or adjusting prices.
- Use ACL for admin features and make sure controllers check authorization in _isAllowed() or by DI of the AuthorizationInterface.
- Protect downloads and exclusive assets by storing them outside the public webroot or by sending them through a controller after membership verification.
- Validate input thoroughly and use Magento's escaping and validation helpers to avoid XSS/SQL injection. Use prepared statements and the framework’s models — avoid raw SQL.
- Use HTTPS for all membership operations, especially when handling payments or personal data.
- Log access to exclusive content for audit. Use monolog and don’t store sensitive data in logs.
Performance considerations
Membership checks can become heavy if implemented naively. Some tips:
- Cache membership status in customer session or in a fast store (Redis) keyed by customer id. Invalidate when membership changes.
- Prefer customer group mapping for price/catalog edits — it avoids per-request DB lookups and hooks nicely with Magento cache.
- When filtering product collections, modify the SQL where possible instead of filtering results in PHP (e.g., add attribute filter to collection before load).
- Avoid heavy logic in layout rendering. Precompute flags when customers log in.
Extensibility and maintainability — good practices
If this module will be used across stores or sold, make it clean and plug-friendly:
- Use service contracts (APIs) for important operations: MembershipManagementInterface, MembershipRepositoryInterface.
- Emit events when membership grants or revocations happen (magefine_membership_granted, magefine_membership_revoked). That lets other modules react without changing core logic.
- Keep business rules out of controllers and blocks. Put them in service classes.
- Write automated tests: unit tests for services and integration tests for DB interactions and observer behavior.
- Follow PSR-12 coding style and Magento coding standards. Add static analysis (PHPStan) as part of CI.
- Document public APIs and extension points in README and inline docblocks.
Sample flow: customer buys a membership product
Here’s a recommended flow and how to implement it safely:
- Create membership plans in admin (title, price, duration, is_active).
- Create a simple product for each plan with SKU prefix MF-MEM-123 and price equal to membership price. Use a product attribute to link product > membership_id.
- Customer purchases product via storefront.
- When invoice is paid (or order complete based on your flow), an observer grants membership: create record in magefine_membership_customer, set start/end dates, and optionally set customer group.
- Invalidate caches (or update session) and notify customer with email template.
Observer pseudo-code was shown earlier. Also send transactional email by creating a template and using Magento\Framework\Mail\Template\TransportBuilder.
Admin UI: manage plans
Use a standard UI Component grid for listing plans and a form UI component for edit/create. Make sure to add ACL checks on admin controllers and menu items. For fast building, use uiComponent examples from core modules (Catalog > Product uses complex patterns you can simplify).
Testing and QA checklist
- Unit test service methods that calculate membership expiry, price adjustments, and membership validation.
- Integration test DB schema and repository CRUD operations.
- Manual test: buy membership as logged-in user; verify group assignment, price visibility, and exclusive content access.
- Test guest purchase to ensure vouchers or guest flows are handled correctly (guest to account conversion might be needed).
- Security penetration test for download endpoints and admin ACL enforcement.
- Performance test for pages that change price/catalog queries based on membership.
Example: small service class to check active membership
// app/code/Magefine/Membership/Model/MembershipManager.php
collectionFactory = $collectionFactory;
$this->customerRepository = $customerRepository;
}
public function isActiveForCustomer($customerId)
{
$now = (new \DateTime())->format('Y-m-d H:i:s');
$collection = $this->collectionFactory->create()->addFieldToFilter('customer_id', $customerId)
->addFieldToFilter('start_at', ['lteq' => $now])
->addFieldToFilter('end_at', ['gteq' => $now]);
return (bool)$collection->getSize();
}
}
Use this manager everywhere to keep membership checks consistent.
Emails and UX
Membership needs UX touches:
- Membership landing page with benefits and CTA to buy.
- My Account section with membership status, start/end dates, renewal CTA.
- Member-only pages clearly labeled and a nice upsell flow for non-members to subscribe.
- Transactional emails on grant and expiry with clear instructions to renew.
Use email templates and layout blocks that are easy to override in themes.
GraphQL and REST API considerations
If you expose membership info via API (e.g., headless storefront), do it through service contracts and secure endpoints that verify the access token’s customer identity. Add GraphQL schema types if you expose memberships to PWA frontends.
Edge cases and gotchas
- Customer group collisions: if customer group is used for other logic, mapping membership to group can create conflicts. Plan groups carefully.
- Cache invalidation: price and category caches can show stale data to members after membership changes. Consider programmatic invalidation or avoid caching sensitive pages.
- Multi-store: membership may be store-specific; keep store_id reference and consider pricing per store.
- Subscription vs one-time membership: extend the model for recurring payments if needed (store recurring token reference, integrate with payment provider).
Wrap-up and next steps
Building a membership module in Magento 2 is a very achievable project, but it touches many parts of the platform. My recommended path for a minimal viable membership module:
- Create membership DB and admin CRUD for plans.
- Create product-to-membership mapping and simple purchase flow.
- Grant membership on paid invoice and store relation to customer.
- Implement membership checks in two quick places: product price and content controller access.
- Decide on customer group mapping for scale and caching improvements.
From there, extend with recurring subscriptions, GraphQL endpoints, and granular permissioning within the membership (example: tiers like Silver/Gold/Platinum with different catalog access).
SEO and Magefine-specific notes
To keep this module SEO friendly for magefine.com and stores hosted by Magefine, follow these tips:
- Use human-friendly SEO landing pages for memberships with schema.org markup to highlight benefits to search engines.
- Avoid blocking search engines from indexing public membership landing pages. Only block actual members-only product pages if they should be private.
- When using member-only products, provide teaser content accessible to search engines — this helps product discoverability while preserving the exclusive content behind a CTA.
- Keep fast response times: Magefine hosting customers expect performance; use Redis/full page cache and minimal per-request DB calls on catalog pages.
Final checklist before going to production
- All input validation and server-side permission checks in place.
- Admin ACL correctly defined and wired into controllers.
- Membership status check is cached and updated correctly on changes.
- Integration tests and some real-world QA: buy, renew, lapse, and force-expire scenarios.
- Backup and migration plan for the membership tables.
If you want, I can prepare a repo skeleton with the files above and a working example that ties a simple product to a membership, assigns a customer group on invoice pay, and demonstrates an afterGetFinalPrice plugin — ready to install on a dev box. Tell me which pieces you want first (admin UI, product mapping, grant/observer, or pricing plugin) and I’ll scaffold it.
Good luck — and if you deploy this on Magefine hosting, consider the caching and group-mapping tips above to keep things fast and secure.
Happy coding, and ping me if you want a repository scaffolded with these pieces.