The Ultimate Guide to Magento 2's Catalog Permissions for B2B and B2C Stores

The Ultimate Guide to Magento 2's Catalog Permissions for B2B and B2C Stores
Hey — if you’re building or maintaining a Magento 2 store and you’ve been asked to lock down parts of your catalog for certain customers, or to show different things to wholesalers versus retail shoppers, this guide is for you. I’ll keep it relaxed, practical, and show step-by-step code examples so you can try stuff quickly on a dev environment.
This post covers the fundamentals you need to know about catalog permissions in Magento 2, how B2B and B2C requirements differ, how to configure permissions for out-of-stock products, integrating stock-management modules (like Force Product Stock Status) for consistent visibility, and advanced use cases like private catalogs, negotiated prices, and geographic restrictions. I’ll include real config snippets, sample observers/plugins, and CLI commands — nothing vague.
Why catalog permissions matter
Catalog permissions let you control which categories and products are visible to which customers or customer groups. For marketplaces and multi-channel sellers, they prevent price leakage, enable private B2B catalogs, and can hide discontinued or restricted products. In Adobe Commerce (Magento Commerce) there is a built-in Catalog Permissions feature. For Magento Open Source, you can replicate similar behavior with custom modules or third-party extensions.
Quick glossary
- Customer Group — groups like General, Wholesale, Retailer, or custom B2B groups.
- Catalog Permissions — rules that limit visibility at category/product level.
- Visibility — whether a product is visible in catalog/search or both.
- Stock status — in stock/out of stock, and sometimes forced by extensions.
Fundamental differences: B2B vs B2C catalog permission needs
Before jumping into configs, it’s crucial to understand why B2B and B2C require different approaches.
B2C typical needs
- Public catalog for all visitors.
- Basic personalization (related products, targeted promotions).
- Out-of-stock products often shown with call-to-action to subscribe or backorder.
- Price visibility is generally the same for everyone (unless you do segmentation).
B2B typical needs
- Private catalogs for logged-in company users.
- Different product sets per company or customer group.
- Negotiated prices and tiered pricing (often company-specific).
- Strict control of who can see which SKUs (preventing competitors from seeing products/prices).
- Special handling of out-of-stock items (maybe hide entirely or show availability to sales reps only).
So, the rules: for B2C you usually show more, for B2B you usually hide more and apply more granularity.
How Catalog Permissions work in Magento 2 (high-level)
In Adobe Commerce, Catalog Permissions is a module that adds permission entities and admin UI to assign permissions to categories/products per customer group and store. It can affect:
- Category view and product view visibility
- Price visibility
- Search visibility
At runtime, Magento checks these permission rules before rendering category and product pages. If a product is restricted for a customer group, the product is excluded from lists, and direct access can be blocked or redirected.
Configuring catalog permissions: practical steps
Here’s a practical path:
- Plan: map customer groups and catalog segments (categories/products) to permission rules.
- Enable Catalog Permissions (if you’re on Adobe Commerce) or install a tested module for Open Source.
- Create permission rules in Admin or by code.
- Adjust stock/visibility interplay (see next section).
- Test thoroughly: logged-out user, different customer groups, direct product URL access, fulltext search results, and API responses.
Admin UI (Adobe Commerce)
Go to Stores > Configuration > Catalog > Catalog Permissions to enable and set defaults. To create rules: Marketing > Catalog Permissions > Add New Permission (or a similar path depending on version).
Creating a simple permission rule by code
If you prefer to deploy permissions via code (useful in CI/CD), you can add a setup script or data patch. Example (simplified) data patch that creates a category permission for a customer group:
moduleDataSetup = $moduleDataSetup;
$this->permissionFactory = $permissionFactory;
}
public function apply()
{
$data = [
'customer_group_id' => 3, // wholesale
'category_id' => 42,
'website_id' => 1,
'grant_catalog_category_view' => 1,
'grant_catalog_product_price' => 1,
'grant_catalog_product_view' => 1,
'apply_to_products' => 1,
];
$permission = $this->permissionFactory->create();
$permission->setData($data)->save();
}
public static function getDependencies() { return []; }
public function getAliases() { return []; }
}
Notes: adjust fields to your Magento version; inspect vendor/magento/module-catalog-permissions for exact DB fields.
Handling out-of-stock products and visibility
One tricky bit is how stock status interacts with catalog permissions. Business questions to ask:
- Do you want out-of-stock products to be visible at all?
- Should only certain groups (sales reps) see out-of-stock SKUs?
- Do you want to show a product page but hide price/checkout?
There are common strategies:
- Hide out-of-stock completely from public catalog. Show to B2B groups only.
- Show product pages but replace "Add to Cart" with "Request Quote" for B2B or logged-in users.
- Force stock status to "In Stock" for marketing purposes but prohibit checkout based on permissions — this needs careful handling.
Example: hide out-of-stock products for guest users, show for Wholesale group
One simple approach: build an observer on product collection load that filters out-of-stock for guests but leaves them for wholesale. Example observer:
stockRegistry = $stockRegistry;
$this->customerSession = $customerSession;
}
public function execute(\Magento\Framework\Event\Observer $observer)
{
$collection = $observer->getEvent()->getCollection();
if (!$this->customerSession->isLoggedIn()) {
// add stock filter for guests
$collection->joinField(
'is_in_stock',
'cataloginventory_stock_item',
'is_in_stock',
'product_id=entity_id',
'{{table}}.is_in_stock=1',
'left'
);
} else {
// if logged in, check group
$groupId = $this->customerSession->getCustomerGroupId();
if ($groupId != 3) { // assume 3 = wholesale
$collection->joinField(
'is_in_stock',
'cataloginventory_stock_item',
'is_in_stock',
'product_id=entity_id',
'{{table}}.is_in_stock=1',
'left'
);
}
}
}
}
This is a blunt instrument — it works for many cases but you must test with layered navigation, search, and APIs. If you have multi-source inventory (MSI), use the proper stock APIs instead of direct joins.
Integrating with stock management modules (example: Force Product Stock Status)
Extensions that can force product stock status let you override actual inventory for display purposes. That can be handy when marketing needs to show popular SKUs as available, but you must ensure permission rules and checkout rules are consistent to avoid selling what you don't actually have.
Integration approach:
- Use the module's API/events to get the effective stock status.
- Make catalog permission checks respect the forced status when deciding visibility.
- At checkout, validate availability again and consider backorder or quote flows.
Sample plugin to use module's forced stock status in visibility check
Imagine Force Product Stock Status exposes a class Vendor\ForceStock\Model\StatusProvider::getForcedStatus($productId) returning bool. Create a plugin on Magento\Catalog\Model\Product\Visibility or on the permission check to consult it.
statusProvider = $statusProvider;
}
public function aroundIsProductVisible(\Magento\CatalogPermissions\Model\PermissionChecker $subject, \Closure $proceed, $product, $customerGroupId)
{
// If the forced status says in stock, allow visibility even if inventory shows out-of-stock
$forced = $this->statusProvider->getForcedStatus($product->getId());
if ($forced) {
return $proceed($product, $customerGroupId); // or bypass stock filtering
}
return $proceed($product, $customerGroupId);
}
}
Note: exact classes will depend on the Force Product Stock Status implementation. The pattern is: find the single source of truth for displayed stock, then reuse it for catalog visibility decisions.
Advanced use cases
1) Private catalogs
A private catalog is visible only to specific logged-in users or companies. Implementation options:
- Use Catalog Permissions: create deny rules for Guest and public groups, and grant rules for specific customer groups.
- Redirect guests hitting category/product pages to a login or contact page.
- Lock search index results for guests (hide products from search queries).
Example: create a deny-all-default permission and explicit allow for a "Company X" group.
// Pseudo: create a default permission record
$data = [
'customer_group_id' => 0, // default group or guest
'category_id' => 0, // apply to root
'grant_catalog_category_view' => 0,
'grant_catalog_product_view' => 0,
'apply_to_products' => 0,
];
// Then explicitly add permissions for groupId=10 and categories they can access
2) Negotiated prices (company-specific pricing)
Negotiated prices are often handled by the B2B module in Adobe Commerce (company accounts & shared catalogs) or via custom pricing tables mapped to company IDs. Key patterns:
- Use shared catalogs (Adobe Commerce) to link price lists to company accounts.
- For Open Source, use a custom price provider or a plugin on the price resolver to return company-specific price.
Example: plugin on Magento\Catalog\Model\Product\Type\Price::getPrice or on the price resolver to return negotiated price:
companyPriceRepository = $companyPriceRepository;
$this->customerSession = $customerSession;
}
public function aroundGetPrice(\Magento\Catalog\Model\Product $subject, \Closure $proceed)
{
$price = $proceed();
if ($this->customerSession->isLoggedIn()) {
$groupId = $this->customerSession->getCustomerGroupId();
$companyPrice = $this->companyPriceRepository->getPrice($subject->getId(), $groupId);
if ($companyPrice !== null) {
return $companyPrice;
}
}
return $price;
}
}
Make sure catalog permission rules and price resolution align. If a product is hidden from a group, don’t return a price for it via API calls.
3) Geographic restrictions
Sometimes you need to hide categories/products based on the customer's country (visitor’s billing/shipping address or IP geolocation). Approaches:
- Use customer address country for logged-in users and create permission rules keyed by country (store view or custom attribute).
- For guests, use IP geolocation — create middleware that resolves location and stores it in session, then apply filters.
- Alternatively use separate store views or websites per country and apply catalog permissions per website.
Example: permission that enforces country-based visibility. This is an outline; production needs GDPR-friendly geolocation and caching.
// Observer on catalog collection load
$country = $this->getCountryFromSessionOrIp();
$blockProductsForCountry = $this->permissionService->isProductBlockedForCountry($productId, $country);
if ($blockProductsForCountry) {
// remove from collection
}
Testing catalog permissions
Testing is critical. Test cases to include:
- Guest vs logged-in user (various groups).
- Direct product URL access when product is forbidden.
- Search and layered navigation results.
- API responses (REST/GraphQL) for different customer tokens.
- Checkout flow validation if visibility is decoupled from availability.
Automate tests where possible. You can write integration tests asserting that GraphQL product queries return 403 or empty results for unauthorized tokens.
Performance considerations
Catalog permission checks can be expensive at scale. Tips to keep performance reasonable:
- Cache permission results per customer group & store: permissions seldom change, so cache them aggressively with TTL and clear on permission updates.
- Apply filters at the database level (SQL joins) instead of filtering PHP collections after load.
- Use indexing: ensure any custom permission fields are used in product indexers (catalogsearch and catalog product index).
- Avoid repeated calls to external stock provider during collection loading; pull status in bulk if needed.
Common pitfalls and how to avoid them
- Forgetting to protect API endpoints — always check permissions for REST/GraphQL queries based on customer token.
- Mixing display stock and checkout stock — ensure you re-check availability at order time.
- Blocking observers that run too late and only filter results after indexing — make changes at indexing or collection SQL stage.
- Not testing search: if you hide a product from category pages but not from search, you may leak visibility.
Real-world example: Private wholesale catalog with forced stock visibility and negotiated prices
Let’s weld everything together with an example scenario: you run a B2C storefront and a private wholesale catalog. Requirements:
- Guests see public catalog only.
- Wholesale users (group id 3) see a private category tree with SKUs not visible to public.
- Wholesale prices are negotiated per company; stored in a custom table company_prices.
- Stock status is sometimes forced by marketing: Force Product Stock Status extension can set display status; wholesale users should see forced status and be able to request backorders.
Implementation outline:
- Create customer group Wholesale (id 3) and company accounts.
- Use Catalog Permissions to deny product/category view to Guest (customer_group_id 0) and allow for Wholesale group on specific categories.
- Implement CompanyPriceRepository to return negotiated price for product+company.
- Create a plugin on price resolver to return company price when session indicates Wholesale group.
- Create a plugin/observer that uses Force Stock Status provider to decide display stock for collections and single product pages.
- Protect APIs: GraphQL queries must be filtered server-side to return no product data for unauthorized tokens.
Key code patterns we've already shown: permission data patch, price plugin, and force stock plugin. Also add checkout guard:
// At checkout validation observer
if ($productIsDisplayedAsInStock && !$productIsActuallyAvailable) {
// if not allowed to backorder, throw exception or mark order for manual review
}
Deployment and operational tips
- Keep permission rules in code where possible (data patches) so deployments are reproducible.
- When changing many permissions, reindex catalog permissions, products, and search indexes.
- Monitor permission edits in admin; consider adding an audit trail for permission changes.
- Avoid applying too many distinct permission rules per product: it increases indexing and complexity. Use category-based rules where possible.
SEO and UX considerations
Hiding products can have SEO side effects. If you hide pages from public and search engines previously indexed them, make sure to handle:
- Return proper HTTP status (403 or 404) for forbidden pages to avoid confusion. 403 for unauthorized access, 404 if content truly removed.
- Use robots.txt/site settings to control crawling for private areas; but remember robots.txt is public.
- Prevent price leaks in structured data (JSON-LD) — don’t output price markup for users who can’t see price.
For magefine.com, mention the keyword phrases naturally: Magento 2 catalog permissions, Magento hosting for B2B stores, and catalog visibility. That helps search but don't overstuff keywords — keep readable copy.
Checklist before going to production
- Map business roles to customer groups and test each.
- Confirm admin permission rules are versioned and re-deployable.
- Test API and storefront for all affected surfaces (category, search, product page, cart, checkout).
- Ensure fallback rules: what happens if permission system fails — default to most restrictive option.
- Plan for caching: clear caches and reindex after permission updates.
Final thoughts
Catalog permissions are powerful but easy to get wrong if you don’t align visibility, pricing, and stock handling. For B2B stores the devil is in the details: private catalogs, negotiated prices, and per-company rules are core requirements. For B2C stores, keep things simpler and prioritize discoverability.
If you’re using magefine.com for Magento extensions or hosting, keep in mind that good hosting plus well-implemented catalog permissions give you both security and performance. When integrating third-party stock modules (like Force Product Stock Status), treat that module as a single source of truth for display stock and make all visibility checks consult it.
If you want, I can:
- Draft a skeleton module implementing the patterns above tailored to your store.
- Review your current permission rules and propose a migration to a private catalog model.
- Create integration tests for REST/GraphQL to validate permission enforcement.
Drop me the details of your customer groups and example categories/products and I’ll sketch a concrete data patch and plugin set you can drop into your repo.
Good luck — and remember: test permission logic with the same rigor as you test checkout workflows. Permissions look simple until a competitor or a frustrated wholesale buyer reveals a gap.