- A buyer searches for a product and sees it in the results.
- The reverse can be worse: a product is deliberately hidden from that organisation, yet an old link or guessed identifier still opens it through the commerce API.
- Both failures come from the same architectural mistake.
- For a B2B marketplace built on Medusa, product visibility depended on the buyer's organisation, active property, delivery geography, supplier approvals, explicit exclusions and supplier-spec
The client problem
A buyer searches for a product and sees it in the results. They click. The product page returns nothing.
The reverse can be worse: a product is deliberately hidden from that organisation, yet an old link or guessed identifier still opens it through the commerce API.
Both failures come from the same architectural mistake. The marketplace has more than one way to read its catalogue, and each path has developed its own interpretation of “visible”.
For a B2B marketplace built on Medusa, product visibility depended on the buyer's organisation, active property, delivery geography, supplier approvals, explicit exclusions and supplier-specific shopping lists. Elasticsearch handled discovery. Medusa REST endpoints hydrated current product, price and inventory data.
We built one access resolution for the business decision, then translated that result for each technical read path. Search and REST did not need identical query languages. They needed identical answers to the question that mattered: may this buyer see this product here?
The client problem: one catalogue, several legitimate views
In a consumer shop, “published” can look like a sufficient visibility rule. In a B2B marketplace, publication is only the beginning.
One property may be served by a supplier while another property in the same organisation is outside its delivery zone. Procurement may exclude a supplier globally. A particular product may be disallowed while the rest of that supplier's range stays available. Compliance may require the supplier to be approved before any of its products appear. A negotiated shopping list may restrict one supplier to a precise assortment while leaving other suppliers unrestricted.
These rules combine. They do not replace one another.
That creates a practical storefront requirement: every catalogue surface must respect the same buyer context. Search results, recommendations, product comparison, live-data hydration and direct product URLs cannot each improvise their own subset.
The business does not care whether one answer came from PostgreSQL and another from Elasticsearch. It expects one coherent catalogue.
Why “published” is not the same as “visible to me”
Medusa owns the commercial product lifecycle. A product must be published and not deleted before a buyer-facing path should consider it.
But a published product can still be inappropriate for a particular buyer.
Suppose Supplier A has products A1, A2 and A3. The buyer's organisation has agreed a shopping list containing only A1. Supplier B has no such restriction. The correct result is not “show only A1 everywhere”. It is “show A1 from Supplier A, while leaving Supplier B's range available unless another rule removes it”.
Now add an organisational exclusion for Supplier A. Exclusion must win: A1 cannot reappear merely because it is on the list. Add credentialing, and an empty set of approved suppliers must mean no suppliers—not “the filter was empty, so show everything”.
Visibility is an algebra of sets and precedence. Treating each rule as an optional UI filter is not enough.
Resolve the business decision once
The core of the implementation is an organisation-access resolution tied to the current request.
It gathers the product IDs explicitly excluded for the organisation, suppliers excluded by procurement policy, suppliers allowed by credentialing, property-credit access and the per-supplier products permitted by active shopping lists. The active property contributes the geographic context used for deliverability.
The result is expressed as ordinary business sets: excluded products, excluded suppliers, allowed suppliers and a map from a restricted supplier to its allowed product IDs.
That shared representation matters more than sharing one database query. PostgreSQL relations, Medusa graph filters and Elasticsearch bool clauses have different capabilities. A stable intermediate decision lets each adapter use its native query model without silently changing policy.
The resolution is memoised for the lifetime of the request. If several parts of one response need access policy or buyer-specific price lists, they read the same resolved context rather than racing through separate interpretations.
Compile the resolution for Elasticsearch discovery
Elasticsearch is the catalogue-discovery surface. It needs to apply policy before ranking and pagination, not remove forbidden products after returning a page.
The project therefore converts the shared resolution into Elasticsearch filters. Geographic deliverability becomes a nested query over indexed delivery zones. Explicit product and supplier exclusions become negative clauses. A credentialed-supplier set becomes a positive allow-list.
Supplier-specific shopping lists need more care. For each restricted supplier, a product is allowed only when its ID belongs to that supplier's approved set. Suppliers absent from the restricted set remain unaffected.
The bool expression says, in effect: either this product belongs to a supplier without a list restriction, or it belongs to a restricted supplier and appears in that supplier's allowed products.
An empty allowed set deliberately hides every product from that supplier. This is not an implementation curiosity; it is the safe business meaning of “this supplier is restricted, but no products are currently approved”.
Do not let the browser choose its own permissions
Location and exclusion parameters can look like convenient search-query inputs. For an authenticated buyer, they are authority decisions.
If the browser can send “no excluded suppliers” or substitute another postal code without server validation, a technically correct Elasticsearch filter still enforces the wrong policy.
For requests carrying organisation context, the marketplace resolves geography and restrictions on the server from the active property and organisation. Buyer-supplied exclusion and geographic query parameters are ignored for that path.
Unauthenticated or legacy search can still use the older public parameters where appropriate. The important boundary is clear: once a request represents an identified organisation, the organisation's policy comes from trusted application state.
This is why visibility cannot be reduced to hiding cards in React. The server decides which catalogue the buyer is entitled to search.
Guard Medusa hydration with the same decision
Elasticsearch returns a fast, search-oriented product document. The storefront may then ask Medusa for authoritative live fields such as current pricing or inventory.
That second request is a security and consistency boundary.
For ID-scoped hydration, the platform runs the requested identifiers through the shared access resolution. Explicit product exclusions are removed. Products from excluded or unapproved suppliers are removed. If a supplier has a shopping-list restriction, only identifiers on that supplier's list survive.
The product-detail route applies the same check before querying the published product. A known or guessed ID therefore does not become a shortcut around catalogue policy. The buyer receives a not-found response when the product is outside their view.
For broader REST listing calls, the route applies blocked product IDs and allowed supplier IDs before Medusa executes the graph query. REST remains useful for hydration without becoming a parallel, less protected catalogue.
Make precedence explicit
Most visibility bugs appear where reasonable rules overlap.
An approved shopping-list item must not override a supplier exclusion. A direct product exclusion must still apply when the supplier is otherwise credentialed. A credentialing allow-list containing no supplier must close the catalogue, not disable the rule. A master viewing an organisation globally may skip property-specific list restrictions while organisational exclusions continue to apply.
Those decisions should exist as named product rules before anyone writes an Elasticsearch clause.
In this implementation, negative exclusions remove products regardless of positive list membership. Credentialing is a positive boundary when the organisation requires it. Shopping-list rules affect only the suppliers that have active restrictions. Unrestricted suppliers do not disappear because another supplier has negotiated assortment control.
Clear precedence turns surprising edge cases into predictable catalogue behaviour.
Test sets, not merely query syntax
A filter can be syntactically valid and still encode the wrong business answer.
The parity tests therefore create a small synthetic catalogue with several suppliers and products. One function computes the expected visible IDs using straightforward set logic. A lightweight evaluator applies the generated Elasticsearch clauses to the same products.
The tests compare the resulting sets for the cases that tend to break:
- one supplier restricted to a small shopping list;
- a supplier both excluded and present in a list;
- a restricted supplier with an empty allowed set;
- a catalogue with no restrictions;
- a credentialing allow-list containing one approved supplier;
- an empty credentialing allow-list.
This does not attempt to prove that Elasticsearch and Medusa have identical ranking, pagination or text relevance. It proves the narrower and more valuable invariant: the same business visibility policy keeps and removes the same representative product IDs.
Keep discovery and authority in their proper roles
The goal was not to make Elasticsearch a second commerce engine.
Elasticsearch discovers and ranks a denormalised catalogue projection. Medusa remains authoritative for product lifecycle and live commerce data. The access resolution sits above both read mechanisms because it represents the buyer's commercial context, not a feature of either database.
That separation lets the storefront use each system for what it does well. Search filters before pagination and ranking. REST hydrates a known set with current commerce information. Direct detail access remains protected.
If the organisation changes a supplier exclusion or shopping-list policy, both paths consume the same conceptual decision. The adapters may evolve independently, but the policy does not fork.
Why Medusa was the right foundation
The client needed more than a public catalogue with customer accounts. It needed catalogue access to reflect how organisations, properties, suppliers and negotiated assortments actually operate.
Medusa supplied the product lifecycle, query layer and extension points. Elasticsearch supplied the search performance and filtering model. The project added the organisation-specific policy that connected them.
This is an architectural reason to choose an extensible commerce engine. We did not have to flatten a complex procurement relationship into tags, duplicate catalogues or storefront-only hiding rules. We could preserve one product model and calculate the permitted view for each buyer context.
The technology choice supported the business model instead of forcing the business model into a generic storefront convention.
A practical visibility-parity checklist
Before adding a second catalogue read path, ask:
- What makes a base product commercially eligible?
- Which rules depend on the organisation?
- Which rules depend on the active property?
- Are supplier and product exclusions independent?
- Is credentialing an allow-list or merely a label?
- Can one supplier have a restricted assortment while others remain open?
- What does an empty allowed set mean?
- Which rule wins when an allow-list and exclusion overlap?
- Are authenticated filters resolved server-side?
- Does search filter before pagination?
- Does REST hydration recheck known identifiers?
- Can a direct product URL bypass discovery policy?
- Do tests compare final visible sets across adapters?
- Are ranking parity and visibility parity kept as separate claims?
If those answers are vague, the storefront will eventually contradict itself.
The broader lesson
When a marketplace has several catalogue read paths, do not copy filters from one route into another and hope they stay aligned.
Model the buyer's visibility decision once. Resolve it from trusted organisation and property context. Compile it into Elasticsearch for discovery and Medusa filters for hydration. Recheck direct identifiers. Test rule intersections as visible product sets.
The buyer should experience one catalogue, even when the platform uses several systems to deliver it.
In this project, we built parity as a product contract rather than a promise between teams. A new search route could reuse the resolver and compiler. A new REST surface could reuse the identifier guard. A new visibility rule had one business home before it acquired database-specific expressions. That made coherence maintainable: the next feature did not depend on every engineer remembering every historical filter.
That coherence is not cosmetic. It is what makes negotiated procurement rules, supplier governance and personalised assortments trustworthy at marketplace scale.
