Best practices for clean scope hierarchies
A misconfigured Magento 2 store view configuration often goes unnoticed for weeks, until a customer on the wrong domain sees the wrong price displayed. The scope hierarchy of default, website and store view decides which value actually wins, and anyone who has not internalized this order produces configuration errors that only show up in production.
Table of Contents
- 1. Why store view configuration deserves its own discipline
- 2. The scope hierarchy: default, website, store, store view
- 3. Understanding core_config_data: path, scope, value
- 4. Adding custom configuration values with the correct scope
- 5. CLI workflows: config:set, config:show, config:sensitive
- 6. env.php versus database configuration
- 7. Cache invalidation after scope changes
- 8. Common pitfalls in production
- 9. Scope strategies compared
- 10. Summary
- 11. FAQ
1. Why store view configuration deserves its own discipline
Anyone running Magento 2 with a single website and a single store view barely notices the complexity behind Magento 2 store view configuration. As soon as a second market, a second language or a second brand is added, the simple backend settings page turns into a multidimensional system with three layered levels. Each of these levels can hold a different value for the same configuration path, and Magento resolves which value actually gets delivered according to a fixed rule.
The real risk does not lie in the technology itself, but in the fact that a broken store view configuration rarely surfaces immediately. A value set incorrectly at website level may silently override a deliberately configured store view setting, without any warning appearing in the admin grid. Only when a customer in a specific store sees the wrong shipping option, or receives a transactional email with the wrong sender name, does the root cause become visible. The following sections show how the scope hierarchy actually works and which practices prevent these errors from the start.
2. The scope hierarchy: default, website, store, store view
Magento 2 has four configuration levels, even though the admin backend usually mentions only three visible scopes. At the top sits default, the global fallback level that applies to every website unless a more specific setting exists. Below it comes the website level, which groups several store groups into one economic unit, for example because they share the same payment processing or the same product catalog. Within a website sits the store level, also called a store group in Magento, which defines the product catalog and category tree structure. At the bottom, and most important for store view configuration, sits the store view level: the language and presentation variant actually experienced by the customer in the storefront.
The rule Magento uses to resolve a value is simple, yet frequently misunderstood: Magento first looks for an entry at the most specific level, then at the website level, and finally at default. If no entry exists for a store view in core_config_data, Magento automatically falls back to the website value, and if that does not exist either, the default value applies. In practice this fallback chain means a value once set at store view level remains in place even after the default value is later changed, which is regularly overlooked during migrations and feature rollouts.
<?php
declare(strict_types=1);
namespace Mironsoft\StoreConfig\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
/**
* ViewModel resolving a config value across the Magento scope hierarchy.
*/
final class ScopeResolver implements ArgumentInterface
{
/**
* @param ScopeConfigInterface $scopeConfig Magento scope config reader
*/
public function __construct(
private readonly ScopeConfigInterface $scopeConfig
) {
}
/**
* Resolve a config path for the current store view, falling back
* to website and default scope automatically.
*
* @param string $path Config path, e.g. "general/store_information/name"
* @param int|null $storeId Store view id, null uses current store
* @return string|null Resolved value or null if not configured anywhere
*/
public function resolve(string $path, ?int $storeId = null): ?string
{
return $this->scopeConfig->getValue(
$path,
ScopeInterface::SCOPE_STORE,
$storeId
);
}
}
3. Understanding core_config_data: path, scope, value
Every configuration value technically lands in exactly one table: core_config_data. The structure is deliberately simple: a path column for the config path such as general/locale/code, a scope column holding default, websites or stores, a scope_id column with the numeric id of the respective website or store view, and finally value with the actual content. Anyone who queries this table directly via SQL immediately sees at which level a value was actually set, instead of clicking through several tabs in the backend.
A common mistake in store view configuration happens when developers insert rows directly into core_config_data via a SQL script without invalidating the config cache afterwards. Magento caches configuration values aggressively in the config cache area, and a value inserted via SQL often only becomes visible after the next cron run, unless bin/magento cache:flush config is run explicitly. For production systems the CLI command config:set should therefore always be preferred over direct SQL manipulation, since it triggers cache invalidation automatically.
-- Inspect which scope actually holds a value for a given path
SELECT config_id, scope, scope_id, path, value
FROM core_config_data
WHERE path = 'general/locale/code'
ORDER BY
CASE scope
WHEN 'default' THEN 0
WHEN 'websites' THEN 1
WHEN 'stores' THEN 2
END;
-- Find all store-view-level overrides for a specific website
SELECT ccd.scope_id, ccd.path, ccd.value, s.name AS store_view_name
FROM core_config_data ccd
INNER JOIN store s ON s.store_id = ccd.scope_id
WHERE ccd.scope = 'stores'
AND ccd.path LIKE 'general/store_information/%';
4. Adding custom configuration values with the correct scope
Anyone developing a custom module that offers backend settings must explicitly declare in system.xml, for each field, at which level a value may be overridden. The attributes showInDefault, showInWebsite and showInStore independently control whether a field is visible and editable at the respective level. Careful store view configuration here means consciously deciding: should a value really differ per store view, such as a sender address for transactional emails, or should it stay global, such as an API key for a payment provider that is identical across a whole website.
A scope granted too generously at store view level, in practice, leads editors to accidentally maintain different values per store view where a single unified value was actually intended. Conversely, an overly restrictive scope prevents international teams from configuring their respective store views independently. The rule of thumb: values that relate to language, currency or the legal requirements of a given market belong at store view level, while technical integration values usually belong at website or default level.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="mironsoft_storeconfig" translate="label" sortOrder="200"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>Store Config Extensions</label>
<tab>general</tab>
<resource>Mironsoft_StoreConfig::config</resource>
<group id="general" translate="label" sortOrder="10"
showInDefault="1" showInWebsite="1" showInStore="1">
<label>General Settings</label>
<!-- Store view scope: legitimately different per market/language -->
<field id="support_email" translate="label" type="text"
sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Support Email per Store View</label>
</field>
<!-- Website scope only: shared payment gateway credential -->
<field id="gateway_api_key" translate="label" type="obscure"
sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="0">
<label>Gateway API Key</label>
<backend_model>Magento\Config\Model\Config\Backend\Encrypted</backend_model>
</field>
</group>
</section>
</system>
</config>
5. CLI workflows: config:set, config:show, config:sensitive
The CLI command bin/magento config:set optionally accepts the parameters --scope and --scope-code and allows targeted changes per website or store view without opening the backend at all. For deployment pipelines this is essential, because it lets the entire store view configuration be represented reproducibly in scripts instead of relying on manual clicks in the admin panel. The complementary command config:show returns the effectively resolved value for a given scope and is therefore the most important diagnostic tool whenever a value does not arrive as expected in the storefront.
For sensitive values such as API keys or SMTP passwords there is additionally config:set --lock-env, which stores the value directly in env.php instead of core_config_data, protecting it from accidental changes through the backend. This combination, database-based store view configuration for content values and file-based configuration for security-relevant values, is the recommended approach for production multi-store systems.
#!/usr/bin/env bash
set -euo pipefail
# Set a value only for the German store view (assume store code "de")
bin/magento config:set --scope=stores --scope-code=de \
general/locale/code de_DE
# Set a value for the entire website "main"
bin/magento config:set --scope=websites --scope-code=main \
carriers/flatrate/active 1
# Inspect the effectively resolved value for one store view
bin/magento config:show general/locale/code --scope=stores --scope-code=at
# Lock a sensitive value into env.php instead of core_config_data
bin/magento config:set --lock-env \
payment/mironsoft_gateway/api_key "sk_live_xxx"
# Always flush config cache after direct database changes
bin/magento cache:flush config
6. env.php versus database configuration
A common misunderstanding in store view configuration concerns the boundary between values in app/etc/env.php and values in core_config_data. As a rule: env.php is meant for environment-specific, security-critical or deployment-relevant values, such as database credentials, cache backend configuration or encryption keys. These values differ per environment (development, staging, production) but are identical across all stores within one environment, which is exactly why they do not belong at the store view level of the database.
Values in core_config_data, on the other hand, are meant for content-related, editorially maintainable settings that may differ between stores and should be changeable by business users without a deployment. Anyone who accidentally stores store-specific values in env.php loses the ability to maintain them granularly per store view in the backend, and has to go through a full deployment cycle for every change. Clearly separating these two configuration levels is one of the most important architectural decisions for a maintainable multi-store setup.
7. Cache invalidation after scope changes
Changes to store view configuration are not always immediately visible in the storefront, because Magento operates several cache layers between the database and the delivered page. The config cache itself is usually invalidated automatically for changes made through the backend, but not always reliably for direct database changes or batch CLI commands. In addition, the full page cache stores entire HTML fragments that, at the time of generation, still contained the old configuration value, such as a phone number displayed in the footer.
For production deployments a fixed sequence is recommended: first use config:set or the backend, then run cache:flush config full_page block_html layout, and when Varnish is in use, additionally trigger an explicit Varnish purge for the affected store view. Anyone who automates this sequence into deployment scripts avoids the common situation where a support ticket about an apparently wrong setting is in fact just a caching problem.
8. Common pitfalls in production
The most common pitfall in store view configuration is assuming that a value changed at the default level automatically applies everywhere. If a more specific value already exists at website or store view level, it remains in place and silently overrides the default change. This regularly leads to situations where a global update, such as a new legally required notice, simply does not arrive in a single store view, because someone set an individual value there years ago that nobody remembers anymore.
A second pitfall concerns configuration values fetched programmatically via ScopeConfigInterface::getValue() without an explicit scope parameter. If the second and third parameters are missing, Magento falls back to the current store context, which in the context of a cron job or a CLI command is often the default store rather than the store the code is actually meant to run for. Anyone writing custom modules should therefore always pass the store scope explicitly instead of relying on the implicit context.
<?php
declare(strict_types=1);
namespace Mironsoft\StoreConfig\Cron;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Store\Model\ScopeInterface;
use Magento\Store\Api\StoreRepositoryInterface;
/**
* Cron job iterating explicitly over every store view instead of relying
* on the implicit default-store context.
*/
final class SyncSupportEmails
{
/**
* @param ScopeConfigInterface $scopeConfig Reads config per explicit scope
* @param StoreRepositoryInterface $storeRepository Lists all store views
*/
public function __construct(
private readonly ScopeConfigInterface $scopeConfig,
private readonly StoreRepositoryInterface $storeRepository
) {
}
/**
* Read the support email for every store view explicitly.
*
* @return void
*/
public function execute(): void
{
foreach ($this->storeRepository->getList() as $store) {
// WRONG: $this->scopeConfig->getValue('general/store_information/name')
// would silently resolve against the default store in a cron context.
// RIGHT: always pass the store scope and id explicitly.
$email = $this->scopeConfig->getValue(
'trans_email/ident_support/email',
ScopeInterface::SCOPE_STORE,
$store->getId()
);
if ($email !== null) {
// ... sync $email for this specific store view
}
}
}
}
9. Scope strategies compared
The following overview shows which configuration level is typically appropriate for which kind of setting in store view configuration, and what consequences the respective choice has in operation.
| Value type | Wrong scope | Recommended scope | Rationale |
|---|---|---|---|
| Payment provider API key | Store view | env.php (lock-env) | Security-critical, per environment, not per store |
| Locale code | Global default | Store view | Language must differ per market |
| Catalog price scope | Store view | Website | Prices usually apply uniformly across a website |
| Support email address | Default only | Store view | Market-dependent contact information |
| Cache backend connection | core_config_data | env.php | Infrastructure value, not editorially maintained |
This mapping is not a rigid rulebook, but a guideline oriented around two questions: must the value genuinely differ per store view, and should the value be changeable by business users without a deployment. Anyone who consistently answers both questions for every new configuration value avoids most of the scope-related errors typically found in grown multi-store installations.
Mironsoft
Magento 2 multi store and internationalization
Need store view configuration that survives rollouts?
We analyze existing Magento 2 multi-store setups, clean up the scope hierarchy, and automate configuration through CLI workflows so deployments stay reproducible and error-free.
Scope audit
Review existing core_config_data for conflicting scope overrides
CLI automation
Represent configuration values as reproducible deployment scripts
Module consulting
Plan system.xml scopes for custom modules correctly and future-proof
10. Summary
A clean Magento 2 store view configuration rests on understanding the fallback chain from store view over website to default, and on the conscious decision which values should be visible at each level in the first place. The core_config_data table makes this structure transparent, CLI commands like config:set and config:show make it reproducible and diagnosable. Security-critical values consistently belong in env.php, editorial and market-dependent values in the database.
The biggest lever against misconfiguration lies in the discipline of making scope decisions once, deliberately, and then rolling them out consistently through CLI scripts instead of maintaining values manually in the backend. Anyone who additionally clears the relevant caches after every change avoids the most common source of error in production: values that are technically already set correctly, but not yet delivered due to caching.
Magento 2 Store View Configuration — Key Takeaways
Scope hierarchy
Store view overrides website, website overrides default. Fallback only applies when no more specific value exists.
core_config_data
The scope, scope_id and path columns determine at which level a value actually sits. Directly queryable via SQL.
CLI over backend
config:set --scope=stores/websites for reproducible deployments, config:show to diagnose the effective value.
env.php for security
Security-critical, environment-specific values belong in env.php via --lock-env, not in the database.