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 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 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:

  1. Module → Modules tab → flip the toggle for the module you want on.
  2. If the module has dependencies that aren't on, flip those first. The effective-state chip on the row will stay grey otherwise.
  3. Save the website.
  4. The Modules tab reloads showing the new effective state. New sidebar items appear (Catalog / Customers / Orders) for whatever the module surfaces.
  5. If the module ships configurable knobs (Checkout, Customer), open the Module settings tab and configure. See the per-module reference page.
  6. 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.

Per-module reference