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.