Skip to main content

Overview

Spree ships the machinery an EU merchant needs to answer a data protection request, prove that consent was given, show a lawful sale price, and tell a buyer how long they have to change their mind. Core provides the data model, the services and the API. Management workflows built on top — a request queue, deadline tracking, automated retention runs — are left to your application or to Spree Enterprise.

Subject requests

A person has the right to a copy of their data and the right to have it erased. Both arrive as a Spree::DataRequest, which records that the request was made and how it was answered. A customer opens a request from their account:
The response is 202 Accepted with the request awaiting_confirmation. Nothing happens yet. A request also covers the guest orders placed at the account’s email address, so it waits until it is confirmed from that address. An erasure request also needs the account password, because erasure cannot be undone and an unattended session should not be enough to trigger it.

Confirming a request

Spree publishes data_request.confirmation_requested, carrying a confirmation token. spree_emails sends the person a link to redirect_url with ?token= appended. A storefront that sends its own emails subscribes a webhook endpoint to the event instead: it carries a live credential, so only an endpoint that names it receives it (see Webhooks). redirect_url is used only when it matches one of the store’s allowed origins. Otherwise the link falls back to the store’s storefront URL. The page the link opens confirms the request when the person presses a button:
Confirm on a button press, never when the page loads. Some mail scanners open links and run the page’s scripts, and an erasure cannot be undone.
A request left unconfirmed for 24 hours lapses. Asking again while a request of the same kind is waiting for confirmation, or still being worked on, returns the existing one. A waiting request also gets its email sent again.

Receiving the export

Building the export reads a person’s whole history, so it happens in the background. When it is ready, spree_emails sends a download link to the same address. The link expires after seven days. The download link is shown only to whoever holds the confirmation token, never to the signed-in account. A storefront that sends its own emails reads it with the token:
A store without spree_emails, and without a webhook handler for data_request.confirmation_requested, cannot complete a request a customer raises. Requests staff raise in the admin still work.
Most requests never reach the storefront. They arrive as email to the merchant, often from someone who can no longer sign in, so the same two actions exist in the admin. Staff confirm who they are dealing with themselves, so these requests need no email confirmation:
The dashboard exposes both on the customer page.

What erasure actually does

Erasure means anonymization. Spree::Customers::Anonymize replaces every value that identifies a person — on the account, its address book, the address snapshots on past orders, saved cards, connected identities, live sessions and consent rows — and keeps everything that is a financial record. Orders, payments, tax lines and line items are never touched. They carry their own retention obligation under accounting and tax law, and deleting them would trade one legal duty for another. On order addresses the country, state and a truncated postal code survive deliberately, so the jurisdiction a past sale was taxed in stays provable. A business-to-business order is the narrower case. An address book belonging to a company is left alone entirely — it is shared by every member, and one person’s erasure is no warrant to rewrite it. Where an order recorded delivery to one of the company’s sites, the contact name and phone on that record are erased and the site and trading name stay: they are the company’s details, not the person’s, and taking them would erase the merchant’s record of a lawful sale along with them. A customer.anonymized event is published when it completes.
Anonymization is the only supported erasure path. If your application adds a table holding customer-identifiable data, extend this flow in the same change. A spec in core fails when a personal-data column appears that the anonymizer does not reach.

Extending the export

Spree exports what Spree knows. If your application holds data of its own — a loyalty ledger, support history — add it through the hook:
server/config/initializers/spree.rb
Handlers return a hash, which is merged into the payload. A handler cannot drop what Spree contributes. You can also veto an erasure — an open dispute, a fraud investigation, a legal hold:
server/config/initializers/spree.rb
accepts_email_marketing says what is true now. Proving consent needs to know when it became true and where the person agreed, so the customer also carries email_marketing_consent_updated_at and email_marketing_consent_source, both written automatically whenever the flag moves. Acceptance itself is recorded as an event in Spree::ConsentRecord — at registration, and at checkout for guests who have no account. Each row keeps the purpose, the source, the moment, and a snapshot of the documents shown, including a digest of the text. A merchant who later edits their terms can still show which version a given person agreed to. Consent rows survive erasure. The proof that consent was given outlives the person’s contact details, which are removed from the row.
Cookie consent belongs to your storefront, not to Spree. These records cover consent your backend can act on, such as marketing permission and accepted terms.

Omnibus price history

The Omnibus Directive requires that an announced price reduction shows the lowest price of the previous 30 days. Spree records every base price change in Spree::PriceHistory and exposes the lowest prior price on request:
Recording is on by default and controlled per store with track_price_history, since a non-EU store has no use for it. Retention defaults to 30 days via price_history_retention_days; prune with:

Right of withdrawal

EU buyers may withdraw from a distance sale within 14 days of receiving the goods. Orders expose the deadline:
Response
The window is set per market with withdrawal_period_days, because the right is regional law. It is deliberately separate from return_window_days: a return window is merchant policy running from purchase, often set to 30 days as goodwill, while withdrawal is a statutory right running from delivery. The deadline is computed on read from the order’s latest delivery, falling back to completion while nothing has been delivered. A stored copy would be wrong the moment a parcel arrived.
  • Pricing — price history and the prior_price an Omnibus display needs
  • Customers — the account an export or erasure acts on
  • Markets — where withdrawal_period_days is set, since the right is regional law
  • Events — subscribing to erasure and data-request activity