Customer module

Module id: customer. Dependencies: none.

What it adds:

  • A customer-facing firewall: registration, login (with remember-me), email verification, password reset.
  • An account area at /account/* — profile, password change, address book, order history (when checkout is also on).
  • A sidebar Customers tab in the admin showing every registered customer.
  • One per-website settings table (customer_settings) with the customer-channel mail-from override.

What's per-website

CustomerSettings is one row per website, lazy-created the first time you save the panel. Today it holds two fields:

Column Type Default What it does
mail_from_email string(191) null From: address used for verification + password-reset mail. Falls back to website default when null.
mail_from_name string(128) null Display name on those mails. Falls back to website default → website name.

When neither resolves, CustomerMailer drops the mail and logs at ERROR. The registration / reset flow itself still completes (the customer's account is created, the password is reset) — only the notification email is missing. Watch the cms-public log for Customer verification mail dropped: website has no mail-from configured.

Realistic example

A salon that wants customer-touching mail to come from a friendlier address than the generic site info@:

Module settings → Customer
─────────────────────────
From email: hallo@pmumelanie.nl
From name:  Melanie van PMU by Melanie

Result: verification + reset mail land in the customer's inbox as Melanie van PMU by Melanie <hallo@pmumelanie.nl>. Form-submit mail and order receipts continue to use whatever the General default + their own overrides resolve to.

Per-website list. Columns:

  • Email + verified flag.
  • First/last name.
  • Company, with the VAT number under it.
  • Locale.
  • Marketing opt-in flag.
  • Last login.
  • Created at.

The search box matches email, name, company and VAT number.

Edit a row to change profile fields (including company and VAT number), force-verify the account (skipping the email step, useful when a customer can't receive mail), or delete (cascades to addresses + cart).

Customer registration flow

  1. Visitor opens the Register page (theme template; default at @GsidCmsCore/customer/register.html.twig).
  2. Submits email + password + name, and optionally a company name and VAT number (see Business customers). Validation: password strength (min 8 chars), email shape, captcha-equivalent (the form ships with the same protections as content forms — see ../security.md).
  3. Customer row is created with isVerified = false.
  4. CustomerMailer::sendVerification() mails a link with a single-use, time-limited token.
  5. Customer clicks → /verify-email/<token> flips isVerified = true.
  6. The website firewall lets unverified customers log in but UI surfaces a "verify your email" banner.

If the verification mail bounces / is lost, the resend button on /verify-email is rate-limited and re-issues a new token.

Business customers: company name and VAT number

A customer can carry a company name and a VAT number on the account itself, not only on a saved address. Both are optional.

  • Asked on the registration form and on the account profile page (/account/profile). Admins edit them on the customer in Sidebar → Customers.
  • VAT number is stored normalised: country prefix, no spaces, dots or dashes (be 0123.456.789 becomes BE0123456789). The form only checks the shape (two letters, then 2 to 13 letters or digits). Whether the number exists is checked against VIES at checkout, when it decides VAT, not when the profile is saved.
  • Checkout prefills them for a logged-in customer. The VAT number comes from the default billing address when that has one, otherwise from the account. The company name fills the shipping and billing company where the saved address has none.
  • Saved addresses keep their own company and VAT number, as before. Existing customers start with both fields empty; nothing is copied over from their addresses.

Themes that override the forms. _register_form.html.twig and account/_profile_form.html.twig receive values.company and values.vat_number, and errors.company / errors.vat_number when a submit is refused. The inputs must be named company and vat_number. A theme that overrides either partial and leaves them out keeps working; the customer is just not asked. In templates, the customer entity exposes company and vatNumber (cms_customer().company).

Order history and invoices

With checkout on, /account/orders lists the customer's placed orders and /account/orders/{id} shows one. The order page lists the order's issued invoice and credit notes, each with a PDF link, including one the shop created but has not emailed. Previews are not shown: a number is what makes it an invoice. Both the page and the PDF answer 404 to anyone but the customer who placed the order.

Themes that override account/order_detail.html.twig receive an invoices list (each entry: number, is_credit_note, issued_at, gross_cents, currency, url) and must add the section themselves to show it.

Customer password reset

  1. /password/forgot → submits email.
  2. CustomerMailer::sendPasswordReset() mails a token-bound link (regardless of whether the email matches an account — no enumeration).
  3. /password/reset/<token> → set new password, log in.

Tokens are one-shot and expire (default: 1 hour).

Theme helpers (when customer is enabled)

Full list in ../../twig-helpers.md#customer-module-customer. The form helpers below render opaque themeable forms — themes don't compose them with the generic form_* helpers in ../../forms.md. Override the form templates instead.

{# Header navigation — show login link or account dropdown. #}
{% if is_customer_authenticated() %}
    {% set me = cms_customer() %}
    <a href="/account">Hallo, {{ me.firstName }}</a>
{% else %}
    <a href="/login">Inloggen</a>
{% endif %}

{# Account-area sidebar with the standard links. #}
{{ cms_account_nav() }}

{# Render the login form on a dedicated /login page. #}
{{ cms_login_form() }}

{# Address book with edit / delete buttons. #}
{{ cms_account_addresses_view() }}

Public routes

Route Path Purpose
customer_register /register Sign up
customer_login /login Login (firewall entry point)
customer_logout /logout Logout
customer_verify_email /verify-email/{token} Click target from verification mail
customer_verify_resend /verify-email/resend "Resend" button
customer_password_forgot /password/forgot Request a reset
customer_password_reset /password/reset/{token} Set a new password
customer_account /account Dashboard
customer_account_profile /account/profile Edit profile
customer_account_password /account/password Change password
customer_account_addresses /account/addresses List addresses
customer_account_address_new /account/addresses/new Add an address
customer_account_address_edit /account/addresses/{id}/edit Edit
customer_account_address_delete /account/addresses/{id}/delete (POST) Delete
customer_account_address_default /account/addresses/{id}/default (POST) Set as default

When the checkout module is also enabled, two more:

| customer_account_orders | /account/orders | Order history | | customer_account_order_show | /account/orders/{id} | Single-order detail | | customer_account_order_invoice | /account/orders/{id}/invoices/{invoiceId} | An issued invoice or credit note, as PDF |

Public-side templates (themes can override)

Default Override at
@GsidCmsCore/customer/login.html.twig @website/customer/login.html.twig
@GsidCmsCore/customer/register.html.twig @website/customer/register.html.twig
@GsidCmsCore/customer/password_forgot.html.twig @website/customer/password_forgot.html.twig
@GsidCmsCore/customer/password_reset.html.twig @website/customer/password_reset.html.twig
@GsidCmsCore/customer/verify.html.twig @website/customer/verify.html.twig
@GsidCmsCore/customer/verify_resent.html.twig @website/customer/verify_resent.html.twig
@GsidCmsCore/customer/_login_form.html.twig @website/customer/_login_form.html.twig
@GsidCmsCore/customer/_register_form.html.twig @website/customer/_register_form.html.twig
@GsidCmsCore/customer/_password_forgot_form.html.twig @website/customer/_password_forgot_form.html.twig
@GsidCmsCore/customer/_password_reset_form.html.twig @website/customer/_password_reset_form.html.twig
@GsidCmsCore/customer/_nav.html.twig @website/customer/_nav.html.twig
@GsidCmsCore/customer/account/dashboard.html.twig @website/customer/account/dashboard.html.twig
@GsidCmsCore/customer/account/profile.html.twig @website/customer/account/profile.html.twig
@GsidCmsCore/customer/account/password.html.twig @website/customer/account/password.html.twig
@GsidCmsCore/customer/account/addresses_list.html.twig @website/customer/account/addresses_list.html.twig
@GsidCmsCore/customer/account/address_form.html.twig @website/customer/account/address_form.html.twig
@GsidCmsCore/customer/account/orders_list.html.twig @website/customer/account/orders_list.html.twig
@GsidCmsCore/customer/account/order_detail.html.twig @website/customer/account/order_detail.html.twig

The account/_*.html.twig partials (flashes, profile-form, password-form, addresses-view, address-form) are similarly overridable under the same path. See cms-core/templates/customer/account/ for the complete list.

Email templates

Default Override at
@GsidCmsCore/customer/email/verify.html.twig (+ .txt.twig) @website/customer/email/verify.html.twig (+ .txt.twig)
@GsidCmsCore/customer/email/password_reset.html.twig (+ .txt.twig) @website/customer/email/password_reset.html.twig (+ .txt.twig)

Both HTML + text variants ship; provide both when overriding.

Things this module does NOT do

  • No social login (Google / Apple / etc). Email + password is the only auth path. Passkey support is admin-only at the moment.
  • No GDPR data export / right-to-be-forgotten flow. Operators can delete a customer manually from the admin; there's no scheduled retention or data-portability endpoint.
  • No customer-side language switching beyond what the website's locale list allows. The customer's stored locale overrides the request locale on /account/* pages.
  • No marketing automation. The marketingOptIn flag is recorded on the customer row but no follow-up emails ship out of the box. Hook it up to your provider via the customer entity (out of scope for v1).