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
webhookmodule: 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_addressandorder_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/postcodebecameadministrative_area/postal_code, andshipping_zone_provincebecameshipping_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 GraphQLAddresstypes and the address REST payloads changed with them, and thetypes/customerAddressandlib/locale/countriesmodules were removed (use@evershop/evershop/lib/address:getCountries,getCountryName,isKnownCountryand 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 atscripts/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/captureTransactionsand the PayPal refund route, together with the per-method admin buttons. UsePOST /api/orders/:id/captureandPOST /api/orders/:id/refunds(admin only); the order page shows generic Capture and Refund buttons. Payment methods now declare optionalcapture,voidandrefundhandlers inregisterPaymentMethod. Cash on Delivery orders get their owncod_*statuses, and a migration moves existing awaiting-payment COD orders frompendingtocod_pendingand records an offlineauthorizetransaction for them. The order status no longer changes. - PayPal return page renamed:
/paypal/proccessingis 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 orSearchPage.tsxmust delete the inline pieces from its copy, or the page renders them twice. - Public
?limit=is capped atsystem.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 byevershop seed. sanitize-htmlwas replaced byxssfor rich text. The allow-list is the same; serialization differs cosmetically (<br>instead of<br />), andstylevalues containingurl(javascript:)orexpression()are now dropped. Code that importedsanitize-htmlthrough 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_deliveryrow 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 towebhookLogLimit(default 100) per webhook. - Each request is signed:
X-EverShop-Signature: t=<unix>,v1=<HMAC-SHA256>over<timestamp>.<body>;X-EverShop-Deliverystays the same across retries.passwordis removed from payloads at any depth. - Only HTTPS and public addresses are allowed, checked at connect time. Set
system.webhook.allowPrivateNetworksto 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
NOTICEfile 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 fromcanCapture/canRefund. Neworder_refundedandorder_canceledevents. - 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 insystem.paypal.webhookId), admin refunds with partial-refund tracking,paypal_pendingfor 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_failedandcharge.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 blogsfallback (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.jsonmoves 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.jsoncan 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,__subtextand__item-headinglet one rule restyle every widget; vertical spacing is consistent. The Section widget follows the page column through--page-max-widthand--page-gutterand exposesdata-padding. evershop seedis 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-htmlinto 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:imageURLs are correct for cloud storage.- Requests no longer hang when
NODE_ENVis neitherdevelopmentnorproduction, 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 buildno 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:devcompiles in place without deletingdist/, so a running dev server keeps working.compile:tscnow copies the same non-source files ascompile. - 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 readsrequest.bodywithout body parsing. @evershop/postgres-query-builder2.1.0 addsSelectQuery.getJoins()andpruneUnreferencedLeftJoins().
Dependencies
- Added:
xss;jsdom(dev, for DOM component tests). - Removed:
sanitize-htmland@types/sanitize-html. - The
multer,sharp,axios,undiciand@evershop/postgres-query-builderupgrades, and the removal ofcypress, shipped in v2.2.2.
Upgrade Notes
- Back up your database. This release runs 6 new migrations:
checkout1.0.12,customer1.0.5 andtax1.0.1 (address columns, renames only),cod1.0.0 (moves awaiting-payment COD orders tocod_pending),cms1.5.0 (contact_submission) andwebhook1.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, runscripts/address-formats/rollback-schema.sql(psql --single-transaction -f) while you redeploy the old release: it reverses the address renames and steps thecheckout,customerandtaxmigration records back, but it does not undo thecodstatus change (orders that were awaiting payment staycod_pending) or remove the new tables. - Update
@evershop/evershop, reinstall dependencies, runnpm run build. Use Node.js 20.9 or newer (required since v2.2.2). - Stripe: enable
payment_intent.succeeded,payment_intent.amount_capturable_updated,payment_intent.payment_failed,payment_intent.canceledandcharge.refundedon your webhook endpoint. - PayPal: create a webhook in the PayPal dashboard pointing at
/api/paypal/webhookand save its ID in the PayPal settings. Until then the endpoint answers 503 and the reconciliation cron is the only fallback. The oldpaypalWebhookSecretsetting is gone. - Review your shipping zones under Settings → Shipping. Provinces became regions, and Sell to countries limits which countries checkout accepts.
- If you use custom SQL, extensions or a theme: update address column names and the removed
types/customerAddressandlib/locale/countriesimports; replace calls to the removed payment routes withPOST /api/orders/:id/captureand/refunds; delete the inline pieces from forked page shells (see Breaking Changes). - Themes that depended on
order-firstto place the header's center Area: Areas now render left, center, right in the markup. Check your header. - Rich text is sanitized by
xss; check content that relied on<br />output or onsanitize-htmlbeing installed.