Practical guide
How to operate multiple online stores from one administration
Share common data and workflows while performing every operation in the explicit scope of one store.
In short
One application does not mean one configuration
A multistore can share catalog, customers, or order workflow while domain, prices, content, stock, payment, and permissions differ by store.
Tenant or shop ID is not an optional filter appended at the end. It belongs in identity, unique constraints, caches, events, audit logs, and every background job.
Prepare
Decide what is shared and isolated
Create an ownership matrix for every entity. Vague “usually shared” behavior leads to unreadable override rules.
- Stores, tenants, domains, legal entities, and administrators with their permissions.
- A global, tenant-specific, and overridable matrix for products, prices, content, customers, warehouses, and orders.
- Data isolation, audit, and any regulatory separation requirements.
- An operating strategy: shared database with scope, separate schemas, or databases and the reason for that boundary.
Steps 1 to 3
Make shop context mandatory
Resolve the active store once at input and pass it explicitly rather than keeping it in hidden global state.
1. Model sharing and overrides explicitly
- A global product can keep SKU and technical data; a shop listing keeps visibility, local name, category, and price list.
- Store overrides explicitly with a clear global fallback. Null must not mean inherit, hide, and delete at the same time.
- An order, payment, and document always belong to one shop and cannot change tenant. Bind shared customers to shop-specific consent and history.
- Include shop_id in unique constraints when a value such as slug or local sequence number is unique only inside a store.
UNIQUE (shop_id, slug) Official PostgreSQL constraint documentation 2. Isolate queries, permissions, and async work
- Repositories require ShopId or tenant context and every administration request checks permissions for that shop.
- Cache keys, files, search indexes, exports, and messages include shop_id. Reject missing scope instead of inheriting one from a session.
- A worker loads shop context from the message and rechecks target ownership. Never carry a web session context into a queue.
- PostgreSQL Row-Level Security can add defense in depth but does not replace correct application design and tests.
handler(message.shopId, message.entityId) Official PostgreSQL Row Security documentation 3. Map domains to known stores
- Map Host through an active-domain allowlist to shop ID. Reject an unknown domain and never create a tenant dynamically from it.
- Choose canonical URL, email sender, payment keys, and branding from verified shop context, not directly from an untrusted Host header.
- Administration switches active shop deliberately and keeps it visible. A bulk action lists every affected store explicitly.
- Version and audit configuration: who changed a domain, price list, warehouse, or payment account and when it took effect.
verified host → domain registry → shop_id Official Symfony Security documentation Step 4
Test cross-store leakage first
One store happy path is easy. The critical negative test proves that another store cannot see or change its data.
-
Use the same ID in two stores
Verify repositories, URLs, cache, export, and workers. A request for store A must never return an object from B.
php bin/phpunit --filter TenantIsolation -
Change global and local values
A global change appears only without an explicit override; removing the override restores inheritance.
-
Send a message with the wrong shop ID
The worker rejects it safely and logs the conflict without changing the target object.
When it goes wrong
Common mistakes
A product from another store appears in administration
A query, cache key, or search index lacks tenant scope. Make context mandatory and add an isolation regression test.
An override cannot return to the global value
The model cannot distinguish inherit, explicit null, and custom value. Add an explicit override state and reset operation.
A worker uses configuration from the last web request
Tenant context is global or session-based. Put shop ID in the message and create a fresh isolated context in the worker.
An unknown Host displays the default store
That fallback is a security and SEO risk. Compare against an allowlist and reject unknown hosts.
Done
One administration shares work, not data by accident.
Shop context is mandatory in queries, permissions, caches, and messages. Sharing and local overrides have clear rules and every change remains auditable.