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.
Sidebar → Customers
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
- Visitor opens the Register page (theme template; default at
@GsidCmsCore/customer/register.html.twig). - 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).
- Customer row is created with
isVerified = false. CustomerMailer::sendVerification()mails a link with a single-use, time-limited token.- Customer clicks →
/verify-email/<token>flipsisVerified = true. - 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.789becomesBE0123456789). 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
/password/forgot→ submits email.CustomerMailer::sendPasswordReset()mails a token-bound link (regardless of whether the email matches an account — no enumeration)./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
localeoverrides the request locale on/account/*pages. - No marketing automation. The
marketingOptInflag 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).