PLATFORM

Multi-store

Multi-store

Each shop gets its own MySQL database. A control database (my_store_control) holds the registry and admin logins; nothing a shop stores ever mixes with another's.

my_store_control     stores (registry only)
  ├─ my_store        the original shop — catalogue, config and its users
  └─ ms_<slug>       one database per new store, created on signup

Signing up/start on the storefront takes a name, then POST /stores derives a slug, creates the database, builds all 16 tables from the entities, and seeds a starter theme, settings, categories and filters. The user is sent to /s/<slug>/setup on the admin to create the first account (bcrypt, 12 rounds).

Addressing a store — a store lives on its own subdomain:

local        my-store.localhost:3000       (Chrome/Edge/Firefox resolve *.localhost)
production   my-store.my-store.shop

Next.js middleware reads the host, strips NEXT_PUBLIC_ROOT_DOMAIN, and rewrites internally to /s/<slug>/… — so the visible URL stays acme.example.com/new-arrivals while one route tree serves every store. The apex itself (localhost:3000, my-store.shop) is the landing page where stores are created.

The API resolves the tenant the same way, from, in order: the request's subdomain, the /s/:slug path prefix, an X-Store-Slug header, or ?store=. With none of those it falls back to the default database. www, app, admin, api, static and assets are reserved and never treated as store names.

Signing in<store>/ms-admin redirects to the console carrying its store. The console shows the owner-setup screen when the store has no users yet, and the login screen once it does.

Safari does not resolve *.localhost; use /s/<slug> there, or add hosts entries.

Database names are derived from the slug and regex-checked before use — they are interpolated into CREATE DATABASE, which cannot be parameterised.

Storefronts live at /s/<slug>; the admin's VIEW STORE button opens the one it is managing.