Operators handbook — modules
Backend modules are optional features shipped by the gsid/cms-core
PHP bundle. They're the second sense of "module" the CMS uses — see
the glossary. Toggle per-website on the
admin's Modules tab; configure per-website on the Module
settings tab (when the module ships configurable knobs).
The modules today:
| Module | id | Adds |
|---|---|---|
| Blog | blog |
Articles + tags: scheduled publishing, translations, RSS, helpers. |
| Catalog | catalog |
Products, categories, tags, tax classes, variants. Theme helpers. |
| Customer | customer |
Registration, login, email verification, password reset, account. |
| Cart | cart |
Anonymous + persistent cart with stock snapshot. |
| Checkout | checkout |
Order placement, payment gateways, shipping, order receipts, holiday closures, packing slips, invoices. |
| Shipping | shipping |
Carriers, delivery zones (countries or postcodes), rate tables. |
| Delivery moments | delivery_slot |
The shopper picks a delivery day and time window at checkout: windows per weekday, cut-off, closed dates. |
| Discount | discount |
Promo codes: percentage / fixed / free-shipping / BOGO, capped redemptions. |
| Catalog & volume discounts | catalog_discount |
Automatic price rules: sales and quantity-tiered pricing, no code entered. |
| Gift cards | giftcard |
Gift cards sold as a product, mailed on payment, spent as a balance at checkout. |
| SEO | seo |
Head tags, structured data, sitemaps, llms.txt. On by default. |
| Landing pages | landingpages |
Programmatic SEO: one template + a dataset of rows = N pages. |
| Portfolio | portfolio |
Named photo collections, each backed by a media folder. |
| FAQ | faq |
Questions and answers, optional categories, search, FAQPage data. |
| Newsletter | newsletter |
Double-opt-in capture + throttled bulk campaigns. |
| Review requests | review_request |
One email per order, some days after shipping, asking for a (Google) review. |
| Abandoned cart emails | abandoned_cart |
One email some hours after a shopper left their cart, with a link that puts it back. |
| Vehicle lookup | vehicle_lookup |
License plate → brand/model/year on a form field (NL, via RDW). |
| Vehicle stock | vehicle_stock |
A car dealer's stock on the website (/occasions) and published to the car platforms the site picks (AutoScout24, Marktplaats). |
| VAT engine | tax |
VAT by destination (NL, BE), VAT on shipping, 0% for EU businesses with VIES, VAT report, proof of transport. |
| bol.com | bol |
Stock push to bol.com and order import from it. |
google |
GA4 / Tag Manager after consent, e-commerce events, server-side GA4 purchase when no thank-you page sent it, Search Console and Bing verification, Merchant Center feed. | |
| Meta Pixel | meta |
Meta Pixel after marketing consent, and paid orders through the Conversions API. |
The two discount modules are easy to mix up: discount is a code the
customer types, catalog_discount is automatic repricing. They stack.
See catalog-discount.md.
A gift card is neither: it does not lower the price, it pays part of it.
See giftcard.md.
Dependencies
Modules can depend on other modules. The toggle stays clickable, but the effective state stays off until every dependency is also on.
blog (no deps)
catalog (no deps)
customer (no deps)
cart requires catalog + customer
checkout requires cart + catalog + customer
shipping requires checkout
delivery_slot requires checkout
discount requires cart
catalog_discount requires catalog
giftcard requires cart
seo (no deps)
landingpages (no deps)
portfolio (no deps)
faq (no deps)
newsletter (no deps)
review_request requires checkout
abandoned_cart requires checkout
vehicle_lookup (no deps)
vehicle_stock (no deps)
tax requires checkout
google (no deps)
meta (no deps)
bol requires checkout
The Modules tab shows two things side by side: the stored toggle (your intent) and the effective state (resolver's verdict). When you flip Cart on but Customer is off, the Cart toggle goes blue but the effective chip stays grey with a hint about which dependency is missing.
In practice: enable in dependency order — catalog + customer first, then cart, then checkout.
Why modules exist
Not every tenant runs an e-commerce site. A boutique services site needs Pages + Forms — that's it. Loading the Catalog admin tab, running stock-reservation cron jobs, or shipping the Cart Twig helpers into the sandbox would all be waste.
Modules carve those features into opt-in groups. The admin doesn't
show the Catalog sidebar item until the catalog module is on, the
module's routes 404, its scheduled jobs skip the site, and
cms_products() answers with an empty list.
Cluster vs. per-tenant
The set of modules is cluster-wide — flipping one on across the cluster requires a cms-core release + composer update. What operators control is which tenants get which modules.
| Concern | Where it lives |
|---|---|
| What modules exist | cms-core release |
| Which modules are toggled on for a website | Website.modules JSON |
| Per-website per-module config | Module's own settings entity (lazy-created) |
Per-website configurable knobs (when they exist) live on the module's own database table:
| Module | Settings entity | What's there today |
|---|---|---|
| Blog | (none) | Per-article config lives on the entities themselves. |
| Catalog | (none) | Per-product/-category config lives on the entities themselves. |
| Customer | customer_settings (CustomerSettings) |
Customer-channel mail-from override. |
| Cart | (none) | No per-website knobs today. |
| Checkout | checkout_settings (CheckoutSettings) |
Flat shipping, order-notification recipients, payment methods, order-channel mail-from override, whether to refuse an undeliverable order. |
| Shipping | (none — shipping_carrier / shipping_zone / shipping_rate rows) |
Carriers, zones (countries and/or postcodes) and the rate table. Each row is the unit of config. |
| Delivery moments | delivery_slot_settings (DeliverySlotSettings) |
Time windows per weekday, cut-off time, lead days, days bookable ahead, closed dates. |
| Discount | (none) | No per-website settings entity — each promo code (the Discount entity) is the unit of config. |
| Catalog & volume discounts | (none, catalog_price_rule / catalog_price_rule_tier rows) |
Each price rule is the unit of config: kind, scope, adjustment, window, priority, tiers. |
| Gift cards | (none, gift_card / gift_card_redemption rows) |
Each card is the unit of config: balance, recipient, send date, active. Which products sell them is a flag on the product. |
| SEO | (none) | A switch only. The SEO data lives on the pages, articles, products and categories. |
| Landing pages | landing_dataset + landing_dataset_row |
One row per dataset (template + slug pattern + meta patterns); one row per generated landing page. Plus landing_template for scan results. |
| Portfolio | (none, portfolio_collection rows) |
Each collection is the unit of config: name, slug, media folder, published, order. |
| FAQ | (none, faq_category / faq_item rows) |
Each category and question is the unit of config, and each belongs to one language. |
| Newsletter | newsletter_settings (NewsletterSettings) |
Double opt-in on/off, confirmation subject, newsletter-channel mail-from override. |
| Review requests | review_request_settings (ReviewRequestSettings) |
Days after shipping, when to ask a returning customer again, review link, email subject and text per language. |
| Abandoned cart emails | abandoned_cart_settings (AbandonedCartSettings) |
Hours after the cart was left, when to remind the same shopper again, email subject and text per language. |
| Vehicle lookup | vehicle_lookup_settings (VehicleLookupSettings) |
Offered countries, cache TTL, raw-payload exposure, per-country provider credentials. |
| Vehicle stock | vehicle_stock_settings (VehicleStockSettings) |
Enabled channels and their credentials (encrypted). The cars themselves are vehicle_stock_vehicle rows. |
| VAT engine | tax_settings (TaxSettings) + tax_merchant_profile rows |
The engine switch (off or active), and the VAT profile, versioned: an approved version is never edited. The tax table (rates per country) is cluster-wide, not per website. |
google_settings (GoogleSettings) |
GA4 measurement ID, Tag Manager container ID, Measurement Protocol API secret (encrypted), Search Console and Bing verification codes. | |
| Meta Pixel | meta_settings (MetaSettings) |
Pixel ID, Conversions API token (stored encrypted), test event code. |
Settings rows are lazy — created the first time you save the panel. Read-paths treat "no row" as "all defaults", so a tenant who never opened the Module settings panel still has a working module (with safe defaults: shipping = 0, no payment methods enabled, mail- from inherited from the website default).
Theme helpers per module
Each module ships a *SandboxContributor that adds its Twig functions,
filters and entity getters to the theme sandbox. The full per-module
helper list is in
../../twig-helpers.md → module helpers.
The sandbox allowlist is cluster-wide, not per website. It is built once from every contributor compiled into cms-core, so the names are callable on every site. What the per-website toggle changes is the answer: each helper checks the module itself and returns an empty result where it is off.
Themes ask directly with
is_module_enabled('<id>'),
which answers the effective state, so a module whose dependency is
missing reads as off. Use it to drop the surrounding markup (a cart
badge, a nav link, a section wrapper) and not just the dynamic content.
The helpers themselves also gate: on a website without the catalog
module, cms_products() returns [] rather than throwing.
Theme template overrides per module
Each module ships default templates for its public-facing pages
(product detail, customer login, checkout, etc.). Themes override by
mirroring the path under templates/ in the theme repo:
@GsidCmsCore/customer/login.html.twig → @website/customer/login.html.twig
@GsidCmsCore/checkout/index.html.twig → @website/checkout/index.html.twig
@GsidCmsCore/checkout/email/order_confirmation.html.twig
→ @website/checkout/email/order_confirmation.html.twig
The full list of overridable templates is in each module's reference page below.
Enabling a module — step by step
For super-admins:
- Module → Modules tab → flip the toggle for the module you want on.
- If the module has dependencies that aren't on, flip those first. The effective-state chip on the row will stay grey otherwise.
- Save the website.
- The Modules tab reloads showing the new effective state. New sidebar items appear (Catalog / Customers / Orders) for whatever the module surfaces.
- If the module ships configurable knobs (Checkout, Customer), open the Module settings tab and configure. See the per-module reference page.
- Verify on the public site: visit a hostname and exercise the feature (add a product to cart, register an account, place an order).
Disabling a module
Same flow in reverse. Be careful: disabling Cart with active sessions
in flight is harmless (cart is per-session, no payment state) but
disabling Checkout while orders are in pending_payment will leave
those orders unable to complete via the public site (the route is
gone). Webhooks still process via Messenger as long as the worker is
up.