← Back to blog

Magento 2 Multi Store and Multi Website Setup Guide

Rodrigo Biazzi

One Magento installation can run several shops: different brands, a B2B and a B2C store, or one brand in several countries and languages. The platform supports this through a hierarchy of scopes, and most of the long term pain in multi store projects comes from picking the wrong level at the start. Moving a store view to its own website later means moving customers, prices and orders, and that is not a small change.

This guide explains what each level controls, how to choose, how to route domains to the right scope with Nginx, and how to keep the setup in code so every environment matches.

The scope hierarchy

Magento has four levels:

  1. Global. The whole installation.
  2. Website. Groups stores that share customers, prices and orders, depending on configuration.
  3. Store, called store group in code. Defines the root category, so it decides which catalog tree is shown.
  4. Store view. The language and presentation layer: locale, theme, translated product names and descriptions, CMS content.

Every installation starts with one of each: the Main Website with code base, the Main Website Store, and the Default Store View with code default.

What each level controls

SettingLowest scope
Customer accountsGlobal or website, by configuration
Product pricesGlobal or website, by configuration
Base currencyGlobal or website, following price scope
Payment and shipping methodsWebsite for most settings
Inventory (MSI stocks)Website, through sales channels
Root categoryStore
Locale, theme, display currencyStore view
Product names, descriptions, URL keysStore view

Two settings define most of the behavior. Customer account sharing, under Stores, Configuration, Customers, Customer Configuration, Account Sharing Options, decides whether a customer account works on all websites or only on the one where it was created. Catalog price scope, under Catalog, Catalog, Price, decides whether a product has one price everywhere or one price per website.

bin/magento config:set customer/account_share/scope 1
bin/magento config:set catalog/price/scope 1

For account sharing, 1 means per website and 0 means global. For price scope, 1 means website and 0 means global. Decide these before the store has customers. Changing account sharing from global to per website fails if the same email address exists on more than one website.

Choosing the right level

Use a store view when

Only the language or presentation changes. A shop in English and French with the same prices, stock and customers is one website, one store and two store views. Translations of product names and descriptions are stored per store view.

Use a separate store when

You need a different catalog tree but everything else is shared. Two brands that share customers and checkout settings but show different categories can be two stores under one website, each with its own root category.

Use a separate website when

Prices, currencies, customers, payment methods or stock differ. A B2B shop with its own customer base and payment terms, or a country store with its own currency and warehouse, is a website. When in doubt between store and website for a business that may grow apart, choose website. Merging later is easier than splitting.

Creating the structure

Create websites, stores and store views in the admin under Stores, Settings, All Stores. Codes are permanent identifiers used in configuration and routing, so pick short lowercase codes like b2b or fr. Create the root category for a new store under Catalog, Categories first, then select it when creating the store.

To keep the structure in code, dump it to app/etc/config.php:

bin/magento app:config:dump scopes

This writes the websites, groups and stores to the scopes section of config.php. Commit it, and on other environments bin/magento app:config:import (also run by setup:upgrade) creates the missing scopes. Root category IDs are stored by number, so the categories must exist with the same IDs on every environment, which is usually true when environments are copied from production. The file is explained in the env.php and config.php guide.

Routing domains with Nginx

Magento decides which scope to run from two server variables: MAGE_RUN_CODE and MAGE_RUN_TYPE. The type is website or store, and the code is the website code or store view code. Without them, Magento runs the default website.

With Nginx, map the host name to a code:

map $http_host $MAGE_RUN_CODE {
    default             base;
    shop.example.com    base;
    b2b.example.com     b2b;
    shop.example.fr     fr;
}

map $http_host $MAGE_RUN_TYPE {
    default             website;
    shop.example.fr     store;
}

Put the map blocks in the http context, outside the server block. Then pass the variables to PHP in the location that handles PHP in the Magento sample config:

fastcgi_param MAGE_RUN_CODE $MAGE_RUN_CODE;
fastcgi_param MAGE_RUN_TYPE $MAGE_RUN_TYPE;

Add every domain to server_name and to the TLS certificate. Using website as the type and letting the website's default store view answer is the simplest option. Use store when a single domain should always load one specific store view. More on the rest of the server block in the Nginx configuration guide.

An alternative for languages on one domain is store codes in the URL, such as example.com/fr/, through the web/url/use_store setting. It is simple to set up, but it changes every URL of the store, so decide it before launch.

Base URLs and configuration per scope

Each website needs its own base URLs, set at website scope:

bin/magento config:set --scope=websites --scope-code=b2b web/unsecure/base_url "https://b2b.example.com/"
bin/magento config:set --scope=websites --scope-code=b2b web/secure/base_url "https://b2b.example.com/"
bin/magento config:set --scope=stores --scope-code=fr general/locale/code fr_FR

Use the same pattern for payment methods, shipping, tax and email sender settings. In the admin, the scope switcher at the top left of the configuration page shows which values are overridden where. A common mistake is setting a value at store view scope while debugging and forgetting it, so the default changes later have no effect on that view.

Inventory, indexing and SEO

With MSI, each stock is assigned to one or more websites as sales channels, and each stock combines one or more sources. A country website can then sell from its own warehouse. See the MSI sources and stocks guide.

Every store view multiplies indexed data. URL rewrites, the search index and product data are stored per store view, and price index rows per website and customer group. Ten store views with a large catalog means much longer reindexing and a much bigger url_rewrite table. Do not create store views you do not need.

For several languages or countries, each store view needs correct hreflang tags pointing to its alternatives. That is covered in the hreflang guide.

FAQ

What is the difference between a website and a store view in Magento 2?

A website can have its own customers, prices, currency, payment methods and stock. A store view only changes language and presentation, such as locale, theme and translated content.

Can customers share one account across Magento websites?

Yes. Set customer account sharing to global. With the per website setting, each website has its own customer accounts.

How does Magento know which website to load for a domain?

From the MAGE_RUN_CODE and MAGE_RUN_TYPE server variables, usually set in Nginx with a map on the host name. Without them, the default website loads.

Can I have different prices per store view?

No. The lowest price scope is website. For different prices in the same website, use customer groups, catalog price rules or separate websites.

I am available for freelance Magento and Adobe Commerce work, including multi store and multi website setups. If you need help with this, get in touch.

Related articles

Magento 2 env.php vs config.php Explained

What goes in Magento 2 app/etc/env.php and config.php, how config precedence works, app:config:dump, --lock-env vs --lock-config and keeping secrets out of Git.