Architecture
One operator, many sites
Section titled “One operator, many sites”One business runs several promotional-products storefronts from one codebase, one database and one admin panel. This shapes the whole design:
- One catalog and one body of content, shared. Products, categories, pages and posts are not owned by a site. Each site chooses what it shows through content rules and can override individual fields, slugs and SEO.
- The site comes from the request host.
SiteResolver::forCurrentRequest()matches the host exactly againstsites.domain. Nothing else in the URL says which site you are on. - Staff see everything. There is one staff login for all sites. What a member of staff may do depends on their role, not on a site.
- Each site picks a theme (
sites.theme), so several sites can look different on the same data.
When you change something, the question to ask is: does this hold up across N sites owned by one operator?
Layers
Section titled “Layers”the root app: routes/web.php, AdminPanelProvider, config/roles.php, themes/ |customer-panel |assistant sales forms |catalog |automations content-management |admin contacts locales file-library |custom-fields settings |utils countries placeholders simple-filament-layoutModules only depend downward. The root app composes the modules and holds little logic of its own: the panel provider, the base layout component and the legacy import commands. The table below is the exact graph.
Module dependencies
Section titled “Module dependencies”From each module’s composer.json, web-moves/* entries only:
| Module | Purpose | Depends on |
|---|---|---|
utils |
Shared concerns, the capability registry, the base plugin and policy classes | (none) |
countries |
Country, state and currency lookups, money helpers | (none) |
placeholders |
Placeholder support for merge text | (none) |
simple-filament-layout |
Structured form and infolist layouts | (none) |
locales |
Locales | countries |
contacts |
Addresses and contact details | countries, utils |
settings |
The Settings screen and its sections | utils |
file-library |
The staff file library | utils |
custom-fields |
Custom fields and field sets | simple-filament-layout, utils |
admin |
Staff users, roles, background tasks, activity log, Horizon | custom-fields, settings, simple-filament-layout, utils |
automations |
When / If / Then rules | admin, placeholders, settings, utils |
content-management |
Sites, projection, routing, pages, posts, menus, widgets, SEO, themes | admin, contacts, countries, custom-fields, locales, placeholders, simple-filament-layout, utils |
forms |
Site forms and their submissions | content-management, utils |
catalog |
Products, categories, pricing, suppliers and feeds, search | admin, automations, contacts, content-management, countries, custom-fields, locales, settings, simple-filament-layout, utils |
assistant |
Mister Smoothie, the admin AI assistant | admin, catalog, content-management, custom-fields, utils |
sales |
Customers, quotes, orders, invoices, purchase orders | admin, automations, catalog, contacts, content-management, countries, custom-fields, placeholders, simple-filament-layout, utils |
customer-panel |
The customers’ panel at /account |
contacts, content-management, sales, settings |
tests/Unit/ModuleDependencyTest.php enforces this: a use WebMoves\Other\... in a module’s src/
must be declared in its composer.json, a package listed in the root composer.json must also be
declared by each module that uses it, and the graph must have no cycles. So a lower module can’t
reach upward. Where one needs something from a module above it, the higher module registers into a
registry the lower one owns: Capabilities, SettingsSections, the automations registry,
SiteResource::registerRecordPage().
How a request flows
Section titled “How a request flows”There are three front doors, all in one Laravel application.
Storefront
Section titled “Storefront”routes/web.php, wrapped in the SetActiveSiteTheme middleware:
SetActiveSiteThemeresolves the site from the host and activates its theme, with the manifest’sparentas the view fallback.- The specific routes match first:
quote-cart/*,search,__preview/{document}. /goes toFrontPageController, and everything else to the catch-all/{routable}, which accepts letters, digits,-,_and/.- The
routablebinding (CmsServiceProvider::bindRoutableParameter()) finds an availablesite_documentsrow for (site, path). If there is none it looks inpath_historyfor a 301 or a 410, and otherwise gives a 404. Listing pages paginate as/page/N. SiteDocumentControllerrenders the view named after the source type’s handle (page.blade.php,product_category.blade.php), unless the entity’s own template file or its field set picks another. Views come from the active theme.
A 404 or 410 renders as the site’s own page in its theme (StorefrontErrorPage, wired in
bootstrap/app.php).
Be careful adding top-level routes: the catch-all is broad, so anything new must be registered before it. See Routing.
Admin panel
Section titled “Admin panel”/admin, defined in app/Providers/Filament/AdminPanelProvider.php. Uses the web guard and
WebMoves\Admin\Models\User. User::canAccessPanel() requires a role. Each module contributes its
resources and pages through a plugin. The panel
does not depend on the host: the same panel answers on every domain, and no theme is active there.
Customer account
Section titled “Customer account”/account, defined in app-modules/customer-panel/src/Providers/CustomerPanelProvider.php. A second
Filament panel on the customer guard, with WebMoves\Sales\Models\Customer as its user. Customers
sign in with an emailed one-time code. Colours, name, logo and favicon come from the site matching
the host (PanelBranding), evaluated per request. See
Customer panel.
The per-site projection
Section titled “The per-site projection”The storefront doesn’t work out availability and URLs on each request. Every page, post, product and
category is projected per site into a row of site_documents: whether it is available there, its
resolved path, its SEO, and so on. Routing, listings and search read that table, indexed by site.
Observers rebuild rows when things are saved, through the Reindex facade
(WebMoves\ContentManagement\Sites\Facades\Reindex). Direct updates that skip model events (a bulk
update(), an import) need cms:reindex afterwards.
This is the design’s central idea: the projection is derived data, it can always be rebuilt, and public reads never have to re-derive rules. See Sites and the projection.
Background work and services
Section titled “Background work and services”| Service | Used for |
|---|---|
| MariaDB | All application data |
| Redis | Queues, and cache and sessions in development and production |
| Horizon | Runs the queue workers: Queues and scheduling |
| Meilisearch (through Scout) | Storefront product search: Search |
| Reverb | Websockets for the assistant’s chat |
| Headless Chromium | Sales document PDFs |
Where to go next
Section titled “Where to go next”- Modules: how a module is put together.
- Testing, Authorization, Commands, Deploying.
- The Subsystems group covers each major area.
app-modules/<module>/ARCHITECTURE.mdremains the exhaustive reference for each module.