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.

30 minutes · Multistore

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

  1. A global product can keep SKU and technical data; a shop listing keeps visibility, local name, category, and price list.
  2. Store overrides explicitly with a clear global fallback. Null must not mean inherit, hide, and delete at the same time.
  3. An order, payment, and document always belong to one shop and cannot change tenant. Bind shared customers to shop-specific consent and history.
  4. 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

  1. Repositories require ShopId or tenant context and every administration request checks permissions for that shop.
  2. Cache keys, files, search indexes, exports, and messages include shop_id. Reject missing scope instead of inheriting one from a session.
  3. A worker loads shop context from the message and rechecks target ownership. Never carry a web session context into a queue.
  4. 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

  1. Map Host through an active-domain allowlist to shop ID. Reject an unknown domain and never create a tenant dynamically from it.
  2. Choose canonical URL, email sender, payment keys, and branding from verified shop context, not directly from an untrusted Host header.
  3. Administration switches active shop deliberately and keeps it visible. A bulk action lists every affected store explicitly.
  4. 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.

  1. 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
  2. Change global and local values

    A global change appears only without an explicit override; removing the override restores inheritance.

  3. 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.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.