docs
    commerce-kit: Overview
    v1.0.1.md

    @sonordev/commerce-kit

    Products, services, event tickets and memberships for Sonor-powered Next.js sites, with checkout built in. Most sites don't sell anything online, and they don't need it. If yours does, you manage what you sell in the Sonor dashboard, and the kit shows it on your pages and carries the visitor through paying.

    Install

    pnpm add @sonordev/commerce-kit

    commerce-kit builds on site-kit. It needs site-kit 8 or newer as a peer dependency, so it shares the one copy your site already has. A site that's still on site-kit 7 raises it in the same step:

    pnpm add @sonordev/site-kit@^8 @sonordev/commerce-kit

    Set site-kit up first, as in Get started: SiteKitLayout in your root layout and SONOR_API_KEY in .env.local. commerce-kit has nothing of its own to configure and adds no environment variable. It needs React 19 and Node 20.19 or newer, and it's ESM only. Next 16 is an optional peer that only memberships/server uses.

    Checkout uses the payment processor you connect to the project in Sonor, Square or Stripe. Memberships and the inline ticket checkout (EventCheckout) need Square.

    Quick start

    A product page and an event checkout. On the site, neither needs more than SiteKitLayout, because the components reach Sonor through the credential it already published.

    // app/shop/[slug]/page.tsx, e.g. /shop/house-blend
    import { ProductPage } from '@sonordev/commerce-kit/commerce'
    
    export default async function Product({ params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params
      return <ProductPage slug={slug} />
    }

    ProductPage loads the product by its slug and shows the gallery, variants, size chart, related products and an inline checkout.

    // components/ticket-checkout.tsx
    'use client'
    
    import { useEffect, useState } from 'react'
    import { EventCheckout, fetchOffering } from '@sonordev/commerce-kit/commerce'
    import type { CommerceOffering } from '@sonordev/commerce-kit'
    
    export function TicketCheckout({ slug }: { slug: string }) {
      const [event, setEvent] = useState<CommerceOffering | null | undefined>(undefined)
    
      useEffect(() => {
        fetchOffering(slug).then(setEvent)
      }, [slug])
    
      if (event === undefined) return <p>Loading tickets…</p>
      if (event === null) return <p>We couldn’t load that event.</p>
      return <EventCheckout event={event} />
    }

    Render it from any page with <TicketCheckout slug="saturday-cupping" />. EventCheckout shows a row for each ticket tier (or a single ticket when the event has no tiers), collects the buyer's details, and takes the card in the page with Square, in one charge.

    Both examples fetch in the browser. The server entry, @sonordev/commerce-kit/commerce/server, loads the same data on the server, for static paths, 404s, metadata and pages that render with the product already in the HTML: see Render on the server.

    Modules

    ModuleImportWhat it does
    Commerce@sonordev/commerce-kit/commerce, /commerce/server and /commerce/utilsProduct pages, grids and cards, services, event calendars and tickets, and checkout.
    Memberships@sonordev/commerce-kit/memberships and /memberships/serverSquare-billed membership plans, an emailed sign-in link, members-only pages, and canceling on your site.

    The package has six entries. Import anything that runs from one of the five module entries. The root has only types, so a stray root import can't pull checkout or server code into a page.

    EntrySideWhat's in it
    @sonordev/commerce-kitTypes onlyCommerceOffering, CommerceCategory, CommerceVariant, CommerceSchedule, OfferingType, Cart, CartItem, EventCheckoutProps and CheckoutLineItem. Nothing that runs.
    @sonordev/commerce-kit/commerceClientComponents, hooks and the browser calls.
    @sonordev/commerce-kit/commerce/serverServer onlygetOfferings, getOfferingBySlug, getUpcomingEvents, getNextEvent, their *Result variants, the static-path helpers, generateOfferingMetadata and createServerConfig.
    @sonordev/commerce-kit/commerce/utilsAnywhereformatPrice, formatDate and the other formatting helpers, plus icsForEvent, googleCalendarUrl and icsResponse. No React, so a route handler or server component can call them.
    @sonordev/commerce-kit/membershipsClientMembershipCheckoutButton, MemberSignInForm, startMembershipCheckout and requestMemberSignIn.
    @sonordev/commerce-kit/memberships/serverServer onlycreateMembers, getMembershipPlans, getMembershipByEmail and cancelMembershipByEmail.

    How it fits with site-kit

    commerce-kit is a kit: it enhances site-kit and never rebuilds it. A site has one transport to Sonor, one credential and one analytics pipeline, and commerce-kit uses those instead of carrying copies. It imports from exactly three site-kit entries, each documented:

    WhatFrom site-kitHow commerce-kit uses it
    Browser calls to SonorsonorFetch, getClientApiConfig and readAnalyticsIdentity, from @sonordev/site-kit/runtimeEvery call from the browser goes through them: the site credential, retries with a timeout, one refresh of an expired token, and a pause after an authentication failure. Calls that start a payment, a registration or a membership are never retried on their own. Checkout and registration also send the visitor's analytics session id, when there's one, so Sonor can attribute a sale to the visit.
    Server calls to SonorserverApiFetch, resolveServerApiConfig, SITE_CACHE_TAGS and isNextControlFlow, from @sonordev/site-kit/servercommerce/server and memberships/server read SONOR_API_KEY and SONOR_API_URL through them, with a time limit and retries, and cache commerce data under the commerce tag so an edit in Sonor refreshes the pages that show it.
    AnalyticstrackEvent, from @sonordev/site-kit/analyticsThe product and checkout components report shopping events with it (see Analytics). An event waits for site-kit's analytics to load, and a site with analytics turned off sends nothing.

    site-kit is a peer dependency and is never bundled into commerce-kit, so your site has one copy of each of those. A few things stay where they were:

    • One credential. The only variable is SONOR_API_KEY, kept on the server. The components use the credential SiteKitLayout already published to the browser.
    • Your CSS keeps working. The components keep their site-kit- class prefix (site-kit-product-grid, site-kit-calendar-event and the rest) because sites style those names. The prefix is a promise to your stylesheets, not a statement about which package ships the component.
    • Brand tokens come from site-kit. The components read the same --sk-* tokens as forms and booking, such as --sk-primary. Import @sonordev/site-kit/brand.css once for the defaults and override them in your own CSS.

    Building another kit? Take the transport from @sonordev/site-kit/runtime, as commerce-kit does: see Building a kit. The other kits build on site-kit's server and client entries, and Kits lists them.

    Moving from site-kit 7

    Through site-kit 7.4, commerce lived inside site-kit at @sonordev/site-kit/commerce. In site-kit 8 it lives here, and nothing is left behind at the old paths. The code is the same: the package name changes and the subpath stays exactly as it was. Memberships never shipped in a site-kit release, so it's new to everyone.

    The short version

    pnpm add @sonordev/site-kit@^8 @sonordev/commerce-kit
    npx sonor-setup codemod --only site-kit-8           # dry run: prints the changes, writes nothing
    npx sonor-setup codemod --only site-kit-8 --write   # applies them
    pnpm build

    The codemod needs sonor-setup 7.3.1 or newer. npx fetches the latest unless the project already has sonor-setup installed; if it does, update that copy first. The codemod reads every TypeScript and JavaScript file in the project, wherever it sits, except node_modules, build output and public/. It rewrites the imports in the table below, adds @sonordev/commerce-kit to your package.json and raises site-kit's range to ^8, and flags anything it can't rewrite, such as a require() of an old path (commerce-kit is ESM only) or an offerings option with no reader, so you can finish those by hand.

    • Nothing is written until you pass --write.
    • --only site-kit-8 runs just this move. Leave it off to run every move.
    • --check exits 1 while a move is pending and never writes, which suits CI. A manual follow-up counts as pending.
    • --path <file> limits the run to one file.

    The originals of files it writes go to a temporary folder outside your project, and running it twice changes nothing the second time. In a monorepo, run it in each app that imports commerce.

    No codemod handy? The swap is mechanical: change the package name in each import and add commerce-kit to package.json. If your package manager reports that commerce-kit needs @sonordev/site-kit >=8.0.0 (npm stops with an ERESOLVE error, pnpm and Yarn warn), site-kit is still on 7.

    What changed

    Before (site-kit 7)After (site-kit 8 and commerce-kit 1)
    @sonordev/site-kit/commerce@sonordev/commerce-kit/commerce
    @sonordev/site-kit/commerce/server@sonordev/commerce-kit/commerce/server
    @sonordev/site-kit/memberships@sonordev/commerce-kit/memberships
    @sonordev/site-kit/memberships/server@sonordev/commerce-kit/memberships/server
    Commerce types imported from the root, @sonordev/site-kitThe same names, from @sonordev/commerce-kit
    sonorMcpServer({ offerings: { path: '/shop' } })sonorMcpServer({ offerings: { path: '/shop', read: getOfferingsResult } }), with getOfferingsResult from @sonordev/commerce-kit/commerce/server

    The memberships rows only matter if you tried a pre-release build. The root types are CommerceOffering, CommerceCategory, CommerceVariant, CommerceSchedule, OfferingType, Cart, CartItem, EventCheckoutProps and CheckoutLineItem.

    // Before (site-kit 7)
    import { OfferingList, ProductPage } from '@sonordev/site-kit/commerce'
    import { getOfferingBySlugResult } from '@sonordev/site-kit/commerce/server'
    import type { CommerceOffering } from '@sonordev/site-kit'
    
    // After (site-kit 8 and commerce-kit)
    import { OfferingList, ProductPage } from '@sonordev/commerce-kit/commerce'
    import { getOfferingBySlugResult } from '@sonordev/commerce-kit/commerce/server'
    import type { CommerceOffering } from '@sonordev/commerce-kit'

    The one change that isn't a package-name swap is the opt-in list_offerings agent tool, which lists priced products, services and events. It used to read the commerce fetchers directly. It takes its reader as an option now, so site-kit doesn't depend on commerce-kit:

    // Before (site-kit 7)
    sonorMcpServer({ businessName: 'Example Studio', offerings: { path: '/shop' } })
    
    // After (site-kit 8)
    import { getOfferingsResult } from '@sonordev/commerce-kit/commerce/server'
    
    sonorMcpServer({ businessName: 'Example Studio', offerings: { path: '/shop', read: getOfferingsResult } })

    path is still optional and defaults to /shop. read is getOfferingsResult: when a request fails, the tool tells the agent the catalog is unavailable instead of reporting an empty one. The plain getOfferings from the same entry also works, but it reads an outage as an empty catalog. If read is missing, sonorMcpServer throws when the server is built, with a message that says what to pass. A site that doesn't pass offerings changes nothing, and neither does list_services: it reads the site's core data, not commerce.

    Your package.json, before and after:

     "dependencies": {
    +  "@sonordev/commerce-kit": "^1.0.0",
    -  "@sonordev/site-kit": "^7.4.0",
    +  "@sonordev/site-kit": "^8.0.0",
     }

    What didn't change

    • The components. Names, props, hooks and what they render are the same code, now built and shipped from commerce-kit.
    • Your CSS. The site-kit- class names, the data-category and data-offering-type attributes, and the --sk-* brand tokens are as they were.
    • Your credential. SONOR_API_KEY is still the only variable. There's no new one.
    • Sonor. Your products, events and payment connection stay as they are, and the components call the same Sonor endpoints.
    • The rest of site-kit. @sonordev/site-kit/commerce and /commerce/server are the only entries site-kit 8 removes.

    A few things are different besides the import path. When a server fetch fails, the log line starts with [commerce-kit] instead of [site-kit/commerce], so update any log filter or alert that matches the old prefix. The shopping events (product_view, add_to_cart, begin_checkout, checkout_start and purchase) now reach Sonor; on a site using SiteKitLayout, site-kit 7 never sent them. Checkout no longer fails in a browser set to block site data. On the server, commerce/server is server only now, returns whole offerings (the gallery, size chart and options included) and caches each response for 60 seconds under the commerce tag, which Sonor expires when you edit an offering; pass { revalidate: 0 } in the config if a page needs every request fresh. A product marked sold out keeps its page and comes back with status: 'sold_out', so if your own markup draws a product, check status before showing a buy button. And the package has its own peers: site-kit 8 or newer, React 19, and Next 16 as an optional peer. See Versions.

    Check that it worked

    npx sonor-setup codemod --only site-kit-8 --check   # exits 0 when nothing is left to rewrite or finish by hand
    
    grep -rnE "@sonordev/site-kit/(commerce|memberships)" . \
      --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.mjs' \
      --exclude-dir=node_modules --exclude-dir=.next
    
    grep -rnE "from ['\"]@sonordev/site-kit['\"]" . \
      --include='*.ts' --include='*.tsx' --exclude-dir=node_modules --exclude-dir=.next
    1. The check lists nothing left to rewrite or finish by hand, and the first search finds no old paths. The root is types only apart from SITE_KIT_VERSION, so the second search finds only type imports and that constant, and none of them should name the root types listed above.
    2. pnpm build passes. An import you missed fails the build and names the path, because nothing is left behind at the old paths.
    3. pnpm ls @sonordev/site-kit @sonordev/commerce-kit shows site-kit 8.x and commerce-kit 1.x.
    4. Load a product page and an event page, and start a checkout. If you sell memberships, load the sign-in page too. In the server log, lines that start with [commerce-kit] are fetch errors from commerce/server.
    5. npx sonor-setup verify still exits 0.

    Questions

    Do I need commerce-kit if I do not sell anything?

    No. If your site doesn't import anything from @sonordev/site-kit/commerce, there's nothing to move: raise site-kit to ^8 whenever you like.

    Can I stay on site-kit 7?

    Yes. A ^7 range never picks up 8, and everything in 7.4 keeps working, commerce included, at @sonordev/site-kit/commerce. Two things to know: commerce-kit needs site-kit 8, so a site on 7 can't install it, and memberships exists only in commerce-kit. If an update raised your range to 8 before you were ready, pin it back with pnpm add @sonordev/site-kit@^7.4.0.

    Will visitors notice the change?

    Only for the better. The components render the same markup with the same class names, call the same Sonor endpoints with the same credential, and check out the same way. What a visitor can see: a product marked sold out keeps its page, shown as unavailable, instead of a 404, and an edit in Sonor reaches your pages sooner. In your server log, fetch errors start with [commerce-kit].

    Does the bundle get smaller?

    No, and it was never meant to. site-kit's root entry was already types only and nothing in its core referenced commerce, so a visitor to a site without commerce never downloaded any of it. On a site that does sell, the components are the same code, so a page loads about what it did before. What shrinks is site-kit's package: about 8% fewer bytes to download and about 13% fewer on disk than 7.4.0. That's install size, not page weight.

    Why is this site-kit 8, and not a 7.x release with the old paths still working?

    Taking a published import path away is a breaking change, so it's a new major version, and a ^7 range wouldn't pick it up by surprise. Keeping the old paths alive would have meant a second copy of the code, or site-kit depending on a package that depends back on it. It wouldn't have saved you an edit either, since commerce-kit has to be added to package.json either way.

    Versions

    @sonordev/commerce-kitNeeds @sonordev/site-kitAlso needs
    1.x8.0.0 or newerReact 19 and Node 20.19 or newer. Next 16 is optional and only memberships/server uses it.

    On site-kit 7 and earlier, commerce is built in at @sonordev/site-kit/commerce and commerce-kit isn't an option. What changed in each release is in the changelog.