<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nathan Allen</title>
    <description>The latest articles on DEV Community by Nathan Allen (@nathan_allen).</description>
    <link>https://dev.to/nathan_allen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4170899%2F0a49d9e8-9064-41e2-a232-95f0fc757d2e.jpg</url>
      <title>DEV Community: Nathan Allen</title>
      <link>https://dev.to/nathan_allen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nathan_allen"/>
    <language>en</language>
    <item>
      <title>Why We Kept Shopify: Connecting Content, Classes and Commerce for a Fitness Retailer</title>
      <dc:creator>Nathan Allen</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:38:32 +0000</pubDate>
      <link>https://dev.to/nathan_allen/why-we-kept-shopify-connecting-content-classes-and-commerce-for-a-fitness-retailer-5fm0</link>
      <guid>https://dev.to/nathan_allen/why-we-kept-shopify-connecting-content-classes-and-commerce-for-a-fitness-retailer-5fm0</guid>
      <description>&lt;p&gt;Before recommending any change, we needed to know whether the storefront was failing. That question decided the whole project. A rebuild fixes storefront limits, but it does nothing for gaps between systems.&lt;/p&gt;

&lt;p&gt;The client was a US fitness retailer with about 30 employees. It sold equipment online through Shopify. It also offered digital training programs and outdoor workout classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Found in the Existing Setup
&lt;/h2&gt;

&lt;p&gt;We reviewed Shopify commerce, fitness content, class registration tools, and HubSpot marketing. Shopify handled products, shopping carts, and secure checkout. Everything else lived elsewhere.&lt;/p&gt;

&lt;p&gt;Workout programs, coaching information, and class schedules were maintained separately from the store. Nothing linked content, products, and classes into a clear path. A customer reading about a program had no direct route to buy gear or register.&lt;/p&gt;

&lt;p&gt;Customers often navigated separate pages to purchase equipment or book a class. Internally, the team maintained duplicate content across disconnected systems. Marketing also lacked a connected view of customer movement between content, purchases, and bookings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why We Kept the Existing Commerce Platform
&lt;/h2&gt;

&lt;p&gt;The review showed that the main issue was poor coordination between systems. It was not the Shopify storefront. Products, carts, and checkout were doing their jobs.&lt;/p&gt;

&lt;p&gt;Replacing Shopify would have meant rebuilding functions that already worked. The retailer needed content, products, and classes to work as one customer journey. It did not need a new transaction engine.&lt;/p&gt;

&lt;p&gt;Keeping the storefront also kept the project practical for a mid-sized business. We connected the existing systems instead of replacing any of them. That decision shaped every step that followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Defined the System Responsibilities
&lt;/h2&gt;

&lt;p&gt;Once we kept Shopify, the next question was ownership. Each system needed a clear role. Without one, the same information would keep living in several places.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shopify: products, shopping carts, and secure checkout&lt;/li&gt;
&lt;li&gt;Strapi CMS: workout programs, trainer profiles, articles, and class information&lt;/li&gt;
&lt;li&gt;HubSpot: customer segmentation and email marketing&lt;/li&gt;
&lt;li&gt;APIs: connections between fitness content, Shopify products, and class-registration tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fitness content was the piece with no real home. Moving it into Strapi gave it a structured place to be managed. Shopify stayed the owner of commerce. This Strapi Shopify integration worked because neither system took over the other’s job.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Connected the Workflows
&lt;/h2&gt;

&lt;p&gt;The integration brought together four elements: commerce, content, marketing, and registration. Each had a defined role within the overall system. We left the existing registration tools in place and connected them instead of replacing them.&lt;/p&gt;

&lt;p&gt;APIs linked Strapi content to relevant Shopify products and to the class-registration tools. Content could now lead customers toward equipment and bookings. Those links removed the need to hunt across separate pages.&lt;/p&gt;

&lt;p&gt;Structured content also fed HubSpot’s work. With content, purchases, and bookings better coordinated, marketing could support stronger segmentation. That allowed more relevant follow-up emails.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed After Implementation
&lt;/h2&gt;

&lt;p&gt;The results were operational and qualitative. Fitness content connected with Shopify shopping journeys. Equipment purchases and class registrations became easier for customers to reach.&lt;/p&gt;

&lt;p&gt;On the team side, content management became simpler across four fitness content areas. Duplicate content updates were reduced. Strapi gave the team one structured place to manage workout programs, trainer profiles, articles, and class information.&lt;/p&gt;

&lt;p&gt;HubSpot supported more relevant customer marketing. The improvements covered customer convenience, simpler daily operations, and more sales opportunities. We did not measure revenue or conversion changes, so I won’t claim any.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Project Reinforced for Me
&lt;/h2&gt;

&lt;p&gt;Integration has costs. Every connection is a dependency, and someone must own it as platforms change. Teams also need clear rules for which system owns each piece of information.&lt;/p&gt;

&lt;p&gt;A separate CMS is not right for every store. If content is simple and rarely tied to products, Shopify’s native tools may be enough. If the storefront itself blocks a required feature, larger changes make sense.&lt;/p&gt;

&lt;p&gt;For this retailer, the problem sat between systems. So I now ask where friction lives before suggesting a replacement. Write down what each system should own, then look at where information crosses boundaries.&lt;/p&gt;

&lt;p&gt;Read Full Case-Study:- &lt;a href="https://www.gyan.solutions/ecommerce/case-studies/strapi-shopify-integration-fitness-retailer/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gyan-ecommerce&amp;amp;utm_content=blog-nathan-allen-why-we-kept-shopify-fitness-retailer" rel="noopener noreferrer"&gt;Strapi &amp;amp; Shopify Integration for a Fitness Retailer&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>shopify</category>
      <category>architecture</category>
      <category>api</category>
    </item>
    <item>
      <title>Duplicate Support Actions Caused by Unhandled Webhook Retries</title>
      <dc:creator>Nathan Allen</dc:creator>
      <pubDate>Mon, 19 Oct 2026 09:28:41 +0000</pubDate>
      <link>https://dev.to/nathan_allen/duplicate-support-actions-caused-by-unhandled-webhook-retries-4386</link>
      <guid>https://dev.to/nathan_allen/duplicate-support-actions-caused-by-unhandled-webhook-retries-4386</guid>
      <description>&lt;p&gt;The webhook endpoint returns success, the support ticket appears, and everything looks normal. Then the same ticket is created again. This can happen without a faulty model because webhook delivery and business-event processing have different reliability guarantees.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical ecommerce order event that creates a customer-service ticket and sends a confirmation message. If the platform retries that delivery, the handler may repeat both actions. This article follows that failure through event identity, PostgreSQL constraints, durable work and tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Successful Webhook Delivery Does Not Guarantee Single Processing
&lt;/h2&gt;

&lt;p&gt;Webhook providers generally promise at-least-once delivery, not exactly-once delivery. If your endpoint times out, returns an error, or loses the connection before the provider records your acknowledgment, the provider cannot know whether you processed the event, so it sends it again.&lt;/p&gt;

&lt;p&gt;The dangerous case is a handler that does the work before responding. It creates the ticket, then a slow database call delays the response past the timeout. The provider sees a failure, the ticket already exists, and the retry creates another.&lt;/p&gt;

&lt;p&gt;I separate two ideas here. Receiving an event means the payload arrived and was stored. Completing the business action means the ticket exists exactly once and the customer was notified once. Acknowledgment should depend on the first, never on the second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Correct Event Identifier for Deduplication
&lt;/h2&gt;

&lt;p&gt;Shopify’s webhook delivery documentation describes &lt;em&gt;X-Shopify-Webhook-Id&lt;/em&gt; as the identifier for an individual webhook delivery, which makes it the natural key for rejecting a repeated delivery. Treat it as a delivery key; other platforms name and scope identifiers differently, so verify each provider.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;X-Shopify-Event-Id&lt;/em&gt; serves a different purpose. Per the same guidance, it correlates deliveries that arise from one merchant action. If an app has several subscriptions for the same change, each delivery can carry its own webhook ID while sharing an event ID.&lt;/p&gt;

&lt;p&gt;Neither header is your business operation identity. I derive a separate key, such as &lt;em&gt;ticket:order_id:topic&lt;/em&gt;, for the action itself. The delivery ID stops repeated deliveries, while the operation key stops two different events from opening two tickets for one need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Duplicate Actions with Persistent Idempotency
&lt;/h2&gt;

&lt;p&gt;Idempotency means repeating an operation leaves the same result as performing it once. In PostgreSQL, I enforce it with a unique constraint on the delivery ID, so the database, not application memory, decides whether an event is new, even across multiple servers.&lt;/p&gt;

&lt;p&gt;The handler inserts the event row and returns quickly. A separate worker claims pending rows and performs the ticket work, a pattern I look for when reviewing &lt;a href="https://www.gyan.solutions/ecommerce/ai-chatbot-solutions/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=gyan-ecommerce&amp;amp;utm_content=blog-nathan-allen-webhook-retries" rel="noopener noreferrer"&gt;ecommerce AI chatbot solutions&lt;/a&gt;. Storing the event and its job in one transaction keeps both together.&lt;/p&gt;

&lt;p&gt;Workers can crash, so claims need recovery. I record a claim time and let another worker reclaim rows stuck in processing beyond a timeout. That recovery guarantees eventual progress, but it also means the same job can run twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;sql&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;webhook_id&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;    &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;     &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;claimed_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;js&lt;/span&gt;
&lt;span class="c1"&gt;// Illustrative only. HMAC verification omitted. Review and test before use.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`INSERT INTO webhook_events (webhook_id, payload)
   VALUES ($1, $2) ON CONFLICT (webhook_id) DO NOTHING`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Shopify-Webhook-Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// new or duplicate: the event is safely stored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;sql&lt;/span&gt;
&lt;span class="c1"&gt;-- Atomic claim; SKIP LOCKED keeps concurrent workers off the same row&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;claimed_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;webhook_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;webhook_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;webhook_events&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
     &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'processing'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;claimed_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'5 minutes'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;claimed_at&lt;/span&gt; &lt;span class="n"&gt;NULLS&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt;
  &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;SKIP&lt;/span&gt; &lt;span class="n"&gt;LOCKED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;The Race Condition in Check-Then-Insert Logic&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
A SELECT followed by an INSERT is unsafe. Two concurrent workers can both run the SELECT, find nothing, and then both INSERT before either commits. A unique constraint makes the second insert fail atomically, which closes the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Failures Between Event Storage and Ticket Creation
&lt;/h2&gt;

&lt;p&gt;A transaction makes database state transitions atomic, but it cannot roll back a call to a remote ticketing API. If a worker creates the ticket and crashes before marking the row done, recovery reruns the job and creates a second ticket.&lt;/p&gt;

&lt;p&gt;The durable work queue, often called an outbox, records intended side effects in the same transaction as the state change. Where the ticketing API supports idempotency keys, send the operation key with each request so repeated calls can return the original ticket.&lt;/p&gt;

&lt;p&gt;Without downstream idempotency, I would reconcile instead. Before creating a ticket, search the ticketing system for the operation key stored in a custom field or tag. This reduces duplicates but is not perfectly atomic, so concurrent workers still need a claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Replayed and Concurrent Webhook Deliveries
&lt;/h2&gt;

&lt;p&gt;Replay tests turn these assumptions into checks. Run each scenario against a test database and a fake ticketing service that counts calls. The assertion is always the same: one ticket and one notification per business operation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Duplicate deliveries: post the same payload with an identical webhook delivery ID several times and expect one stored row and one ticket.&lt;/li&gt;
&lt;li&gt;Concurrent processing: start parallel workers on one pending event and expect exactly one successful claim.&lt;/li&gt;
&lt;li&gt;Crash after ticket creation: kill the worker before recording completion, then confirm recovery does not create a second ticket.&lt;/li&gt;
&lt;li&gt;Unprotected downstream retries: make the fake API time out after succeeding and confirm reconciliation finds the existing ticket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run the concurrency case many times, because races appear intermittently. A single passing run proves little, while a repeated failure usually points to a missing constraint, an unclaimed job, or an unguarded side effect in the worker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency Must Cover the Business Action
&lt;/h2&gt;

&lt;p&gt;The engineering test is simple. Retrying the same delivery, at any moment and from any worker, must not duplicate the intended business operation. Delivery IDs, unique constraints, durable queues and downstream keys each cover one failure mode, and none replaces the others.&lt;/p&gt;

&lt;p&gt;Also observe real outcomes, not only logs. Count tickets and customer messages per order in the ticketing system and the email provider, because a successful insert in your own database never proves what happened in an external API.&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>postgres</category>
      <category>node</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Headless Ecommerce Is Easy to Explain. System Ownership Is the Hard Part.</title>
      <dc:creator>Nathan Allen</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:08:41 +0000</pubDate>
      <link>https://dev.to/nathan_allen/headless-ecommerce-is-easy-to-explain-system-ownership-is-the-hard-part-4g3c</link>
      <guid>https://dev.to/nathan_allen/headless-ecommerce-is-easy-to-explain-system-ownership-is-the-hard-part-4g3c</guid>
      <description>&lt;p&gt;A headless ecommerce diagram looks clean. A Next.js storefront on top. A commerce API underneath. Two boxes and one arrow.&lt;/p&gt;

&lt;p&gt;Production rarely stays that tidy. Search arrives. Then a CMS, an ERP, a PIM, inventory rules, promotions, tax, analytics, and personalization. Every new box brings its own failure modes.&lt;/p&gt;

&lt;p&gt;When I review these architectures, decoupling is rarely the hard part. Ownership is. The better question is not “should we go headless?” It is this: when we decouple the storefront, which responsibilities did we actually move, and who owns them now?&lt;/p&gt;

&lt;h2&gt;
  
  
  Headless Does Not Remove the Backend
&lt;/h2&gt;

&lt;p&gt;Separating the storefront removes the theme layer. It does not remove the work the platform was doing. Checkout, pricing, inventory, customer accounts, orders, fulfillment, tax, promotions, and product data still exist. They need a clear home.&lt;/p&gt;

&lt;p&gt;In a traditional setup, the commerce platform owns most of that by default. The theme calls platform code directly. When something breaks, there is usually one place to look.&lt;/p&gt;

&lt;p&gt;After decoupling, the same responsibilities can spread across several systems and several teams. This is the diagram most proposals show:&lt;/p&gt;

&lt;p&gt;Customer&lt;br&gt;
   ↓&lt;br&gt;
Next.js / React Storefront&lt;br&gt;
   ↓&lt;br&gt;
API Layer&lt;br&gt;
   ├── Commerce&lt;br&gt;
   ├── CMS&lt;br&gt;
   ├── Search&lt;br&gt;
   ├── ERP&lt;br&gt;
   └── Inventory&lt;/p&gt;

&lt;p&gt;The diagram is easy. The questions it hides are not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which system is authoritative for price and stock?&lt;/li&gt;
&lt;li&gt;Who owns the failure when one API is down?&lt;/li&gt;
&lt;li&gt;What happens when inventory sync is delayed?&lt;/li&gt;
&lt;li&gt;What happens when CMS content references a removed product?&lt;/li&gt;
&lt;li&gt;What happens when pricing differs by region?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the team cannot answer these before the build, it will answer them during an incident.&lt;/p&gt;

&lt;p&gt;Platform choice does not settle this either. Shopify’s Hydrogen is built on React Router and works with the Storefront API and Customer Account API. BigCommerce’s Catalyst is built with Next.js on its GraphQL Storefront API. Both give you a solid frontend starting point. Neither decides ownership for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Problems That All Get Called “Headless”
&lt;/h2&gt;

&lt;p&gt;The request sounds identical every time: “we need headless.” The underlying problems are not identical. I try to separate them first. For the business side of that decision, see &lt;a href="https://www.gyan.solutions/ecommerce/blog/headless-ecommerce-vs-traditional-ecommerce/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=headless-system-ownership&amp;amp;utm_content=nathan-headless-cms-page" rel="noopener noreferrer"&gt;headless ecommerce vs traditional ecommerce&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A frontend problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The store has a slow theme, heavy scripts, and a dated design. But the catalog is standard. Pricing is standard. Checkout works. There is one storefront and few integrations.&lt;/p&gt;

&lt;p&gt;Here the constraint is frontend engineering. Clean up the theme, audit third-party code, fix images and caching. A new architecture would add a deployment pipeline and an API layer to solve a problem that better frontend work can solve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. An experience problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Marketing needs buying guides, product education, campaign pages, and flexible layouts. Every idea has to fit theme templates, and every test needs a developer.&lt;/p&gt;

&lt;p&gt;This is where separating presentation from commerce starts to pay off. Commerce keeps products, carts, and checkout. A CMS and a custom frontend own the experience. Each side can then change without forcing the other to move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. An architecture problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The business has an ERP, a PIM, regional pricing, B2B rules, and fulfillment systems. Orders and stock move between tools. Middleware keeps growing.&lt;/p&gt;

&lt;p&gt;This is bigger than a frontend decision. The storefront is one consumer of a wider system. The real work is deciding where pricing, inventory, and orders live and how those systems agree with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headless Is Not a Performance Strategy by Itself
&lt;/h2&gt;

&lt;p&gt;A Next.js storefront can still be slow. I would not assume otherwise because the repo is modern.&lt;/p&gt;

&lt;p&gt;Real storefronts collect analytics, tag managers, review widgets, personalization, and ad scripts. Slow APIs and uncached calls add latency. Oversized images and heavy client-side JavaScript add more. A decoupled frontend can carry all of that, and sometimes adds new network calls on top.&lt;/p&gt;

&lt;p&gt;Performance depends on engineering: rendering strategy, caching, image delivery, API response times, and script discipline. Headless gives you control over those. It does not do the work for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headless and Composable Solve Different Levels of Separation
&lt;/h2&gt;

&lt;p&gt;Headless separates presentation from the commerce backend. That is one cut.&lt;/p&gt;

&lt;p&gt;Composable architecture makes several cuts. Commerce, CMS, search, PIM, ERP, personalization, and analytics can each be a separate service with its own owner. You choose the best tool per capability and connect them through APIs.&lt;/p&gt;

&lt;p&gt;That flexibility is real. So is the cost. Each additional cut adds integrations, monitoring, vendor relationships, and another place for data to drift. I treat headless as one decision and composable as a bigger one. They should not be bought together by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a CMS Should and Should Not Own
&lt;/h2&gt;

&lt;p&gt;Content is often the first thing teams separate, and for good reasons. Buying guides, landing pages, FAQs, campaign copy, and regional editorial content change often. Editors need previews and structure, not a deploy.&lt;/p&gt;

&lt;p&gt;A well-planned &lt;a href="https://www.gyan.solutions/technology/headless-cms-development/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=headless-system-ownership&amp;amp;utm_content=nathan-headless-cms-page" rel="noopener noreferrer"&gt;headless CMS architecture&lt;/a&gt; gives that content a proper home. But the CMS should not quietly grow into a second commerce system.&lt;/p&gt;

&lt;p&gt;Pricing, inventory, customers, checkout, and orders belong to commerce or to the operational systems behind it. When a CMS stores its own copy of a price or stock level, you now have two sources of truth. One of them will be wrong at the worst time.&lt;/p&gt;

&lt;p&gt;The safer pattern is to store references. CMS content points to a product ID. The frontend fetches live commercial data from the commerce API and combines it with editorial content at render time.&lt;/p&gt;

&lt;p&gt;Next.js supports this with on-demand revalidation by tag or path. A CMS webhook can trigger it when content changes. That keeps pages fresh without rebuilding everything. It also creates a dependency someone must monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Rough Decision Table
&lt;/h2&gt;

&lt;p&gt;This is a starting point, not a rulebook.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgp9pv2ie3yj6w39vfw6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgp9pv2ie3yj6w39vfw6c.png" alt=" " width="690" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decoupled Technology, Undecoupled Responsibility
&lt;/h2&gt;

&lt;p&gt;This is where distributed architecture turns into distributed confusion. The technology is separated. The responsibility is not.&lt;/p&gt;

&lt;p&gt;Here is the pattern I look for in reviews. A price looks wrong on the storefront. Is it the commerce platform, the ERP sync, a regional price list, or a stale cache? If four teams each own a piece and nobody owns the path, the answer takes hours.&lt;/p&gt;

&lt;p&gt;Or take inventory. The ERP is authoritative, but the commerce platform shows stock. A sync runs late. The storefront sells something that is gone. Who decides what customers see when the data is uncertain?&lt;/p&gt;

&lt;p&gt;I want a written answer to a few things for every system in the stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is it the source of truth for?&lt;/li&gt;
&lt;li&gt;Who owns its uptime and its upgrades?&lt;/li&gt;
&lt;li&gt;What does the storefront show when it fails?&lt;/li&gt;
&lt;li&gt;How do we detect that it faile
d?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is exciting. All of it decides whether the architecture survives its second year.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I Would Not Go Headless
&lt;/h2&gt;

&lt;p&gt;I get cautious when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The store has standard requirements&lt;/li&gt;
&lt;li&gt;The existing platform handles checkout well&lt;/li&gt;
&lt;li&gt;One storefront is enough&lt;/li&gt;
&lt;li&gt;Integrations are limited&lt;/li&gt;
&lt;li&gt;The theme can support the experience the business needs&lt;/li&gt;
&lt;li&gt;No engineering team owns the extra infrastructure
Sometimes keeping the monolith is good engineering. A simpler system with clear ownership beats a flexible one nobody can explain.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Question I Start With
&lt;/h2&gt;

&lt;p&gt;Headless gives teams real freedom over the frontend, the content, and the integrations. That freedom is valuable when a specific constraint justifies it.&lt;/p&gt;

&lt;p&gt;So I start with one question. What is the current platform preventing us from doing cleanly? If the answer is a theme, fix the theme. If it is content freedom, separate content. If it is operations, look behind the storefront.&lt;/p&gt;

&lt;p&gt;How have you decided where pricing and inventory live once the storefront was decoupled?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ecommerce</category>
      <category>architecture</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>I Like Strapi for Ecommerce. I Still Wouldn't Start With Strapi.</title>
      <dc:creator>Nathan Allen</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:17:55 +0000</pubDate>
      <link>https://dev.to/nathan_allen/i-like-strapi-for-ecommerce-i-still-wouldnt-start-with-strapi-n19</link>
      <guid>https://dev.to/nathan_allen/i-like-strapi-for-ecommerce-i-still-wouldnt-start-with-strapi-n19</guid>
      <description>&lt;p&gt;I wouldn't start with Strapi.&lt;/p&gt;

&lt;p&gt;I wouldn't start with Sanity, Payload, or any other CMS either. The first decision in an ecommerce stack isn't the CMS. It's simpler than that. Which system owns each piece of data?&lt;/p&gt;

&lt;p&gt;Teams skip that question because choosing a tool feels like progress. Content types get modeled. Pages get built. Then two systems quietly start storing the same price.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership Comes Before the Tool
&lt;/h2&gt;

&lt;p&gt;Before I look at any CMS, I list what the business actually stores. Products and variants. Prices and promotions. Inventory. Cart, checkout, and orders. Editorial content. Search indexes. Customer data. Anything the ERP or PIM already holds.&lt;/p&gt;

&lt;p&gt;Then I ask three questions about each item. Who writes it? Who reads it? What should the storefront do when the owner is down?&lt;/p&gt;

&lt;p&gt;The last question matters most. It's easy to say "the commerce backend owns prices." It's harder to decide what the page shows when that backend times out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Overlapping Ownership Looks Like
&lt;/h2&gt;

&lt;p&gt;*&lt;em&gt;Here's a stack I'd call reasonable:&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Storefront (Next.js)&lt;br&gt;
 ├─ Strapi  → editorial content&lt;br&gt;
 ├─ Medusa  → products, prices, carts, orders&lt;br&gt;
 ├─ ERP     → warehouse stock&lt;br&gt;
 └─ Search  → indexed catalog&lt;/p&gt;

&lt;p&gt;Each box has one job. Strapi holds guides, campaigns, and FAQs. Medusa runs commerce. The ERP owns stock. Search owns discovery.&lt;/p&gt;

&lt;p&gt;Now add one shortcut. A marketer wants a sale price inside a campaign banner. Someone adds a price field to the Strapi content type. Both Strapi and Medusa can now hold an editable price.&lt;/p&gt;

&lt;p&gt;That's where the trouble starts. A sync job gets written. It runs late, or it fails quietly. Say the banner shows 49 and the cart shows 59. No CMS feature fixes that. It's an ownership bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Mistake I See Most Often
&lt;/h2&gt;

&lt;p&gt;The mistake is modeling content before deciding ownership. It feels harmless. A "Product Highlight" type with name, price, stock status, and image looks useful on day one.&lt;/p&gt;

&lt;p&gt;Later, those fields drift. Names change in the commerce backend. Stock changes in the ERP. Editors fix what they can see, and nobody trusts the data anymore.&lt;/p&gt;

&lt;p&gt;Now I write a one-page ownership table first. Every field gets one owner. If the CMS doesn't own a field, it doesn't get a copy of it. It gets a reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store References, Not Copies
&lt;/h2&gt;

&lt;p&gt;A reference is usually a product handle or ID. The CMS stores which product, not what the product is.&lt;/p&gt;

&lt;p&gt;`// Editorial content comes from Strapi&lt;br&gt;
const campaign = await strapi.getCampaign(slug);&lt;/p&gt;

&lt;p&gt;// Product facts come from the commerce backend&lt;br&gt;
const products = await commerce.getProducts(campaign.productHandles);`&lt;/p&gt;

&lt;p&gt;This keeps one source of truth. But it has a cost. Every campaign page now needs two requests. Previews need commerce data too. If the commerce API is slow, the campaign page is slow.&lt;/p&gt;

&lt;p&gt;So I decide the failure behavior up front. If products don't load, the page should still show the editorial content. The product block can hide or fall back. A buying guide shouldn't return a 500 because a price service had a bad minute.&lt;/p&gt;

&lt;p&gt;I'd rather accept two requests than maintain two prices. But it's a tradeoff, not a free win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks, Caching, and Stale Content
&lt;/h2&gt;

&lt;p&gt;Once content and commerce live apart, freshness becomes your job. Strapi can send webhooks on events like entry.create, entry.update, entry.publish, and entry.unpublish. For most events, the payload contains the full entry with its relations and components.&lt;/p&gt;

&lt;p&gt;A common pattern is to call your frontend when an editor publishes. The frontend then revalidates the affected page. In Next.js, that's on-demand revalidation.&lt;/p&gt;

&lt;p&gt;Two details matter to me. First, secure the endpoint. Strapi lets you configure webhook headers, so verify them. Second, don't treat a webhook as a guarantee. I'd add a scheduled reconcile job, so one missed event doesn't leave a page stale for days.&lt;/p&gt;

&lt;p&gt;Preview needs thought too. Editors want to see drafts. The preview route has to read draft content from Strapi and still fetch live commerce data. Test that path early. In my experience it breaks more easily than the published path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Strapi Earns Its Place
&lt;/h2&gt;

&lt;p&gt;I like Strapi when content needs real structure. Components and Dynamic Zones let editors assemble pages from approved blocks. REST and GraphQL let each channel request what it needs. Backend customization is real, and you can self-host it.&lt;/p&gt;

&lt;p&gt;Next to Medusa, the split is clean. Medusa handles products, variants, pricing, carts, and orders through its commerce modules. Strapi handles guides, campaigns, FAQs, and reusable blocks. Different problems, different systems.&lt;/p&gt;

&lt;p&gt;But using both increases what you own. That's two deployments, two upgrade paths, two permission models, and one more integration to monitor. Strapi 5 also changed core APIs, so I check version support before depending on any community plugin. If you want a closer look at the implementation side, this page on &lt;a href="https://www.gyan.solutions/technology/strapi-development-services/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=strapi-ownership-first&amp;amp;utm_content=article-middle" rel="noopener noreferrer"&gt;Strapi development services&lt;/a&gt; covers content models, integrations, and upgrades.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I Wouldn't Add Strapi
&lt;/h2&gt;

&lt;p&gt;The commerce platform already covers your content needs&lt;br&gt;
Nobody owns hosting, upgrades, and security&lt;br&gt;
The team wants fully managed infrastructure&lt;br&gt;
Another CMS fits your editor workflow better&lt;br&gt;
The design needs the CMS to duplicate operational data&lt;/p&gt;

&lt;p&gt;The last one is a design problem, not a tooling problem. If the plan requires copying prices or stock into the CMS, I'd fix the plan before choosing software.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Ask Before Installing Anything
&lt;/h2&gt;

&lt;p&gt;Who owns each field? What happens when that owner is unavailable? How does a content change reach the storefront? Who patches the CMS? Who owns the integration between systems?&lt;/p&gt;

&lt;p&gt;If I can't answer those in plain language, the CMS isn't the next step. A missing answer usually means two systems will fight over the same data later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Principle I Keep
&lt;/h2&gt;

&lt;p&gt;I still like Strapi. I just don't start there anymore.&lt;/p&gt;

&lt;p&gt;I start with one page that says who owns what. Once that page is honest, the CMS choice gets smaller and calmer. Sometimes the answer is Strapi. Sometimes it's something simpler. Either way, the stack has fewer arguments waiting to happen.&lt;/p&gt;

</description>
      <category>strapi</category>
      <category>architecture</category>
      <category>ecommerce</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
