v2.3.0

evershopcommerce/evershopv2.3.0Oct 8, 2026by github-actions[bot]

Upgrading?

AI Summary

A major feature release introducing a country-aware address system, unified payment flows, a new webhook module, and a redesigned media library and email system.

Key Highlights

  • New Webhook module for sending signed events with retry logic and delivery logs
  • Country-aware Address Format Registry for dynamic forms and validation
  • Unified payment capture/void/refund flow across all payment methods
  • Redesigned media library with rename and paste-to-upload capabilities
  • Transactional emails redesigned with a shared layout and 17 languages

Breaking Changes

  • Address columns renamed (e.g., `full_name` → `recipient`) and new columns added
  • Payment capture/refund routes replaced by unified endpoints (`/orders/:id/capture`)
  • PayPal return page URL changed from `/paypal/proccessing` to `/paypal/processing`
  • Storefront pages split into blocks; themes must remove inline code to avoid double rendering
  • Public API limit parameter `?limit=` is now capped at 200 by default
  • Fresh installs no longer create sample data
  • `sanitize-html` library replaced by `xss` for rich text processing

New Features

  • Webhooks module with support for up to 10 HTTPS endpoints
  • Address Format Registry for country-specific forms and validation
  • Unified payment operations (capture, void, refund) for all payment methods
  • Redesigned transactional emails with shared layout and refund/cancellation templates
  • Media library with paginated file browser, rename, and paste-to-upload
  • Theme building blocks allowing storefront pages to be composed of movable components

Full Release Notes


A feature release on top of v2.2.2: outbound webhooks, a country-aware address system, a unified payment capture/void/refund flow with a rebuilt PayPal integration, redesigned transactional emails, a media library, and storefront building blocks for theme authors. It includes everything in v2.2.2 (a start-up fix and several security fixes).

This release contains **breaking changes** and **6 new database migrations**, one of which renames address columns. Please read **Breaking Changes** and **Upgrade Notes** before upgrading. If you upgrade from v2.2.1 or earlier, also read the Upgrade Notes of v2.2.2 below.

## Highlights

* **Webhooks** — a new `webhook` module: send signed events to up to 10 HTTPS endpoints, with retries and a delivery log.
* **Address Format Registry** — country-aware address forms, validation, storage and display, driven by one format record per country.
* **Unified payments** — one capture / void / refund flow for every payment method, and a rebuilt PayPal integration (webhook, refunds, reconciliation).
* **Emails** — a shared email layout, plus new refund and cancellation emails, in 17 languages.
* **Media library** — a paginated, redesigned file browser with rename and paste-to-upload.
* **Theme building blocks** — `layouts.json`, storefront pages split into movable blocks, shared widget typography hooks, theme assets and seed content.

## Breaking Changes

Four changes that also affect you when upgrading from v2.2.1 shipped in v2.2.2 and are described there: raw SQL needs `sql()`, `getContextValue(...)` takes data literals only, `Query.order(uuid)` needs proven access, and API route middleware is authenticated.

* **Address columns were renamed (Address Format Registry).** On `customer_address`, `cart_address` and `order_address`: `full_name` → `recipient`, `address_1` → `address_line_1`, `address_2` → `address_line_2`, `city` → `locality`, `province` → `administrative_area`, `postcode` → `postal_code`; seven nullable columns were added (`organization`, `address_line_3`, `dependent_locality`, `sorting_code`, `given_name`, `family_name`, `extra`). `tax_rate.province` / `postcode` became `administrative_area` / `postal_code`, and `shipping_zone_province` became `shipping_zone_region`. The migrations rename only (no row is rewritten), but **v2.2.1 and v2.2.2 code do not work with the new schema**: they still start, but every address reads back empty. The GraphQL `Address` types and the address REST payloads changed with them, and the `types/customerAddress` and `lib/locale/countries` modules were removed (use `@evershop/evershop/lib/address`: `getCountries`, `getCountryName`, `isKnownCountry` and the address types). Custom SQL, extensions and themes that read the old names must be updated. A script that reverses the address renames only is in the repository at `scripts/address-formats/rollback-schema.sql`.
* **Payment capture and refund routes were replaced by one contract.** Removed: `POST /api/stripe/paymentIntents/capture`, `POST /api/stripe/paymentIntents/refund`, `POST /api/cod/captures`, `POST /api/paypal/authorizations/capture`, `POST /api/paypal/authorizedTransactions`, `POST /api/paypal/captureTransactions` and the PayPal refund route, together with the per-method admin buttons. Use `POST /api/orders/:id/capture` and `POST /api/orders/:id/refunds` (admin only); the order page shows generic Capture and Refund buttons. Payment methods now declare optional `capture`, `void` and `refund` handlers in `registerPaymentMethod`. Cash on Delivery orders get their own `cod_*` statuses, and a migration moves existing awaiting-payment COD orders from `pending` to `cod_pending` and records an offline `authorize` transaction for them. The order status no longer changes.
* **PayPal return page renamed**: `/paypal/proccessing` is now `/paypal/processing` (no alias).
* **Storefront pages are now a shell plus blocks.** The product, category, cart, checkout, account, order list, blog and search pages keep only the query, the provider and the Areas; every piece is its own page component that registers into a slot at its old position, so the default markup is unchanged. A theme that **forked** `ProductView.tsx`, `CategoryView.tsx`, `ProductSingleForm.tsx`, `ShoppingCart.tsx`, `Checkout.tsx`, `MyAccount.tsx`, `OrderList.tsx`, a blog page shell or `SearchPage.tsx` must delete the inline pieces from its copy, or the page renders them twice.
* **Public `?limit=` is capped** at `system.max_collection_size` (default 200, the largest admin grid option).
* **Fresh installs no longer create sample data.** The install migration no longer creates the Men / Women / Kids categories or the Color and Size attributes with their options. Existing stores keep what they have. Themes supply their own catalogue through `themes/<id>/seed/*.json`, read only by `evershop seed`.
* **`sanitize-html` was replaced by `xss`** for rich text. The allow-list is the same; serialization differs cosmetically (`<br>` instead of `<br />`), and `style` values containing `url(javascript:)` or `expression()` are now dropped. Code that imported `sanitize-html` through EverShop must declare it itself.
* **Prices and dates follow the active locale** (the request locale, then the Store Setting language, then `shop.language`). Admin pages format in the admin language.

## New Features

### Webhooks (new `webhook` core module)

* Admin → Settings → Webhooks: up to 10 webhooks, each with an HTTPS URL, a signing secret and a list of events (21 event topics, grouped in the form). Includes a test event, a delivery list with filters, a payload viewer and Retry.
* Delivery is write-first: a `webhook_delivery` row is stored, sent, and retried from that row (after 1, 5, 30 and 120 minutes, 5 attempts). The row is both the retry queue and the admin log. Delivery is at-least-once; a 2xx response within 10 seconds counts as success. Failed deliveries are kept 30 days; completed ones up to `webhookLogLimit` (default 100) per webhook.
* Each request is signed: `X-EverShop-Signature: t=<unix>,v1=<HMAC-SHA256>` over `<timestamp>.<body>`; `X-EverShop-Delivery` stays the same across retries. `password` is removed from payloads at any depth.
* Only HTTPS and public addresses are allowed, checked at connect time. Set `system.webhook.allowPrivateNetworks` to allow local receivers.
* Extensions can add topics with `addProcessor('webhookTopics')`. Translated into all 17 languages.
* Event subscribers placed in `subscribers/_all/` now receive every event, and every subscriber gets `{ name, uuid }` as an optional second argument. Delivery of ordinary subscribers is unchanged (at-most-once).

### Address Format Registry

* One country format record (Google libaddressinput shape) now drives the storefront form, server validation, display, GraphQL, emails and every integration mapping. The checkout and the account address book use one schema-driven form with country-specific fields, region lists, telephone and postal patterns.
* Admin: Settings → Customer (name format, telephone, company, address lines, required fields, default country) and **Sell to countries** on Settings → Shipping, with a confirmation that lists shipping zones that would be left without a country.
* Shipment emails now print the delivery address.
* Public API for extensions: `@evershop/evershop/lib/address` (`patchAddressFormat`, `registerRegionProvider`, `registerAddressField`, `resolveAddressSchema`, `validateAddress`, `formatAddress`, `toIntegrationAddress`). Registration is locked after bootstrap, like the other registries.
* The package now ships a `NOTICE` file for the bundled address data (derived from Google's libaddressinput).

### Payments

* **Unified operations.** Capture, void and refund are owned by core (`captureOrder`, `refundOrder`) and work the same for every payment method. The order page buttons appear from `canCapture` / `canRefund`. New `order_refunded` and `order_canceled` events.
* **PayPal** was rebuilt: payment is finalized in-process on the return page (the old HTTP call to the store's own URL failed behind a bot challenge or proxy), a verified webhook at `POST /api/paypal/webhook` (set the webhook ID in the PayPal settings or in `system.paypal.webhookId`), admin refunds with partial-refund tracking, `paypal_pending` for pending captures, idempotent transaction recording, current Orders v2 payloads with exact-amount breakdowns (including zero-decimal currencies), and a reconciliation cron (every 30 minutes, `system.paypal.abandonedOrderTtlHours`, default 6) that captures approved orders and cancels and restocks abandoned ones. When a buyer cancels at PayPal, the abandoned order is now cancelled and restocked before the cart is reactivated.
* **Stripe**: the charge amount and currency come from the order instead of the browser, the webhook verifies them, locks the order row and handles `payment_intent.payment_failed` and `charge.refunded`.
* **Cash on Delivery** is wired into the same contract with its own `cod_*` statuses.
* Order status changes are clamped instead of throwing on terminal or reverted states; shipments can be cancelled on refunded orders and cannot be created on terminal ones.

### Transactional emails

* A shared layout (header and logo, content, footer; button, divider and items-table partials; a `{{t}}` translation helper) now renders the order confirmation, welcome, reset password, shipment created and shipment delivered emails.
* **New emails**: refund and cancellation, each switchable with `notification_emails.order_refunded` / `order_canceled` (`enabled`, `templatePath`).
* The email copy is translated into all 17 languages (machine-assisted; less common languages deserve a native-speaker review).

### Media library and file browser

* The file browser is paginated for every storage provider (local, S3, Azure Blob, Google Cloud Storage) with cursors, so a folder with thousands of files no longer loads at once. Folders always load completely; listings now include size and type.
* **Rename** files (extension is preserved, an existing name is never overwritten), **paste-to-upload**, a details panel with the full URL, size and dimensions, loading skeletons, and a working scrollbar. A **Media library** entry joins the CMS menu.
* The configured upload types (System Setting → File Uploads: PDF, video, audio) are honoured by the browser; before, only images worked.

### Storefront and catalog

* **All products page** at `/products`, editable in the page builder, with sorting, filters, pagination, a category tree facet and an attribute facet. Included in the sitemap and offered in the link picker. The category filter now matches the whole subtree.
* **Virtual collections** ("newest", "on sale", a category's products) for widgets and the page builder, so a new store's shelves are not empty.
* Product cards decide **Add to Cart** themselves: simple products get the button ("Sold out" when out of stock), products with variants get **Select options**, a link to the product page. The featured items, recommendation, collection spotlight and rich-text product widgets gain the button.
* **Contact form widget** with stored submissions: submissions are saved before the email is attempted, with an admin grid (unread / read / spam) and delete. The email outcome is recorded.
* Admin → landing pages: **Replace homepage** with a landing page, with an automatic disabled backup and a one-click restore.
* New `featured blogs` fallback (newest posts when none are picked), a slideshow placeholder in the page builder, and the page builder shows a loading skeleton while a replaced image renders.

### Theme development

* `themes/<id>/layouts.json` moves any storefront page component to another Area or sort order without forking the file. Hot reload in development; admin pages are never affected.
* Theme `theme.json` can now ship **landing pages**, upload **assets** through the store's configured storage provider (`theme-asset:<path>` tokens) and reference products or collections by a stable key (`store-ref:<entity>/<by>/<key>`).
* Shared widget classes `evershop-widget__eyebrow`, `__heading`, `__subtext` and `__item-heading` let one rule restyle every widget; vertical spacing is consistent. The Section widget follows the page column through `--page-max-width` and `--page-gutter` and exposes `data-padding`.
* `evershop seed` is re-runnable and theme-aware: widgets (`--widgets`), variant groups, category images and a theme's retired categories are handled idempotently, size can drive a variant like colour, and seeded images go through the configured storage provider. The footer copyright is now live (`© <year> <store name>`) when none is set.

## Security

The fixes released in v2.2.2 are listed under that version. This release also fixes:

* A PayPal capture action that could be called without authentication using only an order UUID; the public PayPal capture and authorize routes were removed.
* The Stripe charge amount is now derived from the order, not from client data.
* `?limit=` on public pages could request the whole catalog in one response (denial of service).

## Performance

* The product listing COUNT query drops joins it does not need (about 50.7 ms → 26.5 ms on a 300,000-product catalog) and the category facet matches a subtree with one array parameter instead of a recursive subquery (about 79 ms → 2.5 ms). The store-wide attribute facet uses an EXISTS probe (about 48 ms → 2 ms on 1.2 million index rows).
* Rich text sanitizing no longer bundles `sanitize-html` into the storefront: about 200 KB less minified (67 KB gzipped).
* Image and static-asset requests no longer create a session or send a `Set-Cookie`, so CDNs can cache product images.

## Bug Fixes

* The order confirmation email no longer prefixes the base URL onto absolute (S3 and other remote) thumbnail URLs.
* `og:image` URLs are correct for cloud storage.
* Requests no longer hang when `NODE_ENV` is neither `development` nor `production`, and local extensions no longer vanish in that case.
* A widget with settings its GraphQL type does not accept no longer takes the whole page down with a 500: unknown keys are dropped with a warning, an unusable widget is left out of the page, and saving such settings is refused.
* Sorting on the product listing resets the page, and the direction toggle works on the default sort.
* Display locale for prices and dates comes from the active locale, not only `shop.language`.
* Page components are imported in Area `sortOrder`, so the CSS cascade follows it (headings no longer lose their Tailwind base styles on the cart and checkout pages).
* Menu items are keyed by their stored id (no duplicate React keys); the mobile menu stays inside the viewport; the header renders its Areas in source order (correct keyboard tab order).
* Product images default to `object-fit: cover`, so photos are cropped rather than stretched.
* Banner and slideshow buttons and headlines are legible over photographs; the split feature's vertical alignment works; call-to-action buttons share one height; either slideshow arrows field can hide the controls.
* The Section widget lines up with the page column and no longer causes sideways scrolling on Windows and Linux.
* The gallery slider and the variant selector work with other bundlers and with relative product URLs.
* The file browser opens above the page builder, and its clicks no longer close the settings drawer.
* `evershop build` no longer logs a database connection error (no database exists at build time).
* Seeding no longer dies on the second page, re-uploads category images, or creates a category a theme meant to retire.

## Developer Experience

* New `npm run compile:dev` compiles in place without deleting `dist/`, so a running dev server keeps working. `compile:tsc` now copies the same non-source files as `compile`.
* Jest maps `@components/*` to the compiled components and stubs stylesheet imports, so page components can be unit tested.
* New regression tests guard failures that compile and pass CI: a comment inside `export const layout`, a Node built-in reachable from a React component, and an API route that reads `request.body` without body parsing.
* `@evershop/postgres-query-builder` 2.1.0 adds `SelectQuery.getJoins()` and `pruneUnreferencedLeftJoins()`.

## Dependencies

* Added: `xss`; `jsdom` (dev, for DOM component tests).
* Removed: `sanitize-html` and `@types/sanitize-html`.
* The `multer`, `sharp`, `axios`, `undici` and `@evershop/postgres-query-builder` upgrades, and the removal of `cypress`, shipped in v2.2.2.

## Upgrade Notes

1. **Back up your database.** This release runs 6 new migrations: `checkout` 1.0.12, `customer` 1.0.5 and `tax` 1.0.1 (address columns, renames only), `cod` 1.0.0 (moves awaiting-payment COD orders to `cod_pending`), `cms` 1.5.0 (`contact_submission`) and `webhook` 1.0.0. They apply automatically on first start. **After they run, do not start v2.2.1 or v2.2.2 against the database: it starts, but every address reads back empty.** To go back, restore your backup. As an alternative, run `scripts/address-formats/rollback-schema.sql` (`psql --single-transaction -f`) while you redeploy the old release: it reverses the address renames and steps the `checkout`, `customer` and `tax` migration records back, but it does not undo the `cod` status change (orders that were awaiting payment stay `cod_pending`) or remove the new tables.
2. Update `@evershop/evershop`, reinstall dependencies, run `npm run build`. Use Node.js 20.9 or newer (required since v2.2.2).
3. **Stripe**: enable `payment_intent.succeeded`, `payment_intent.amount_capturable_updated`, `payment_intent.payment_failed`, `payment_intent.canceled` and `charge.refunded` on your webhook endpoint.
4. **PayPal**: create a webhook in the PayPal dashboard pointing at `/api/paypal/webhook` and save its ID in the PayPal settings. Until then the endpoint answers 503 and the reconciliation cron is the only fallback. The old `paypalWebhookSecret` setting is gone.
5. Review your shipping zones under Settings → Shipping. Provinces became regions, and **Sell to countries** limits which countries checkout accepts.
6. If you use custom SQL, extensions or a theme: update address column names and the removed `types/customerAddress` and `lib/locale/countries` imports; replace calls to the removed payment routes with `POST /api/orders/:id/capture` and `/refunds`; delete the inline pieces from forked page shells (see Breaking Changes).
7. Themes that depended on `order-first` to place the header's center Area: Areas now render left, center, right in the markup. Check your header.
8. Rich text is sanitized by `xss`; check content that relied on `<br />` output or on `sanitize-html` being installed.

Related Repositories