Two systems, two truths: why the separation is right
I see the same pattern in almost every conversation with manufacturers and retailers: the product data lives in the ERP or PIM, say SAP Business One or Akeneo. Marketing wants to build landing pages, guides, and campaigns. And someone suggests "just copying everything into the CMS". That suggestion is where most projects later fail: the copy goes stale, maintenance doubles, and in the end nobody knows which price is correct.
A quick note on who is writing here: I'm Matthias, a one-person agency called happycoding.agency in Berlin. I build websites and portals on Sanity and connect them to the systems my clients already run. What Sanity fundamentally is and how the Content Lake works is something I cover in a separate introductory article.
In this article, you get the architecture that has proven itself in my projects: clear data ownership per data type, three sync patterns between ERP, PIM, and Sanity, the typical pitfalls from day-to-day operations, and a decision guide for when you need a PIM at all.
Single source of truth: every data type has exactly one leading system
First, the core principle: for every data type, there is exactly one source of truth. All other systems hold copies at most, and every copy knows it is a copy. As soon as two systems are allowed to own the same field, you no longer have a dataset — you have two opinions.
This is how I split responsibilities in projects with manufacturers and retailers:
| Data type | Leading system | Examples |
|---|---|---|
| Master data, prices, stock | ERP | article numbers, terms, inventory levels |
| Attributes, variants, translations | PIM | technical data, classification via ETIM or ECLASS |
| Editorial content | Sanity | landing pages, guides, brand storytelling, SEO copy |
| Transactions | Shop or ERP | cart, order, invoice |
This holds across industries. At a car dealership, the vehicle data comes from the dealer management system and flows to the listing platforms via feed; the guide page "Why buy a one-year-old car?" belongs in the CMS. At a technical wholesaler, the PIM maintains 40,000 articles with ETIM attributes; the application reports and selection guides are created in Sanity.
From this follows the most important rule of this article: only what editors are responsible for belongs in the CMS. Three things have no place in Sanity, not even "just to be safe" as a copy:
- Prices: They change according to ERP logic, sometimes per customer. When in doubt, a copy in the CMS is wrong.
- Stock: It goes stale by the minute. No sync interval is short enough when the website promises availability.
- Attribute logic: Inheritance, variants, and classification are core tasks of the PIM. Rebuilt in the CMS, they become a second, worse implementation.
The reverse check works the same way: what is maintained in the CMS must not be overwritten by any other system. A product's marketing description can therefore deliberately live in Sanity while the technical description comes from the PIM. Two fields, two owners, no conflict.
Three sync patterns between ERP, PIM, and the Content Lake
Now to the core question: how do the systems come together without data being maintained twice? Three patterns cover almost all cases in my practice. Which one fits depends on how fresh the data on the website needs to be and whether editors should work with it.
Pattern 1: webhook push into the Content Lake
The PIM reports changes, a small service writes them to Sanity. Akeneo ships the Event Platform for exactly this: you subscribe to product events instead of polling the API on a schedule. The older Events API is deprecated and only supported until December 31, 2026 (as of October 2026).
On the Sanity side, the mutation API receives the data. The decisive trick is a deterministic document ID, such as product-{SKU}: with the createOrReplace mutation, every sync updates the same document instead of creating duplicates. A transaction bundles several mutations, so a product update arrives completely or not at all.
For bulk imports, the limits are worth a look: a query-based mutation touches at most 10,000 documents; for larger catalogs, you paginate by _id. For the nightly full sync of 40,000 articles, that means several transactions, ideally with your own transactionId, so you can trace which batch went through if something fails.
You choose this pattern when editors should work with product data in Sanity: they see titles and attributes in the Studio, reference products in landing pages, and the synced fields are read-only because the PIM owns them.
Pattern 2: pull at build or request time
The opposite direction: Sanity stores no product data at all. Your frontend queries both APIs and merges the responses into the page, either at static build time or live per request. I would handle prices and stock this way without exception: live from the ERP endpoint, never from a copy.
With the pull approach, caching deserves a look: Sanity's API CDN answers GROQ queries from cache and takes load off both build and runtime. For editorial content, that is ideal. The price from the ERP, however, you deliberately fetch past the cache — otherwise staleness sneaks back in through the back door.
The cost of this pattern: editors do not see the products in the Studio. For a catalog with stock levels changing hourly, that does not matter; for curated landing pages, it is a real loss. That is why I usually combine pattern 2 with the third one.
Pattern 3: references to external IDs instead of data copies
The Sanity document stores only the key, say the SKU, and the frontend resolves it against the ERP or PIM at runtime. In practice, a middle ground has proven itself: a lean product stub in Sanity with ID, name, and image, while everything else stays out. In the schema, the stub is its own document type with read-only fields and the SKU as a required field.
Editors reference the stub like any other document, and the truth stays in the source system. You get both: curatable product placements in the Studio and current data on the website, without copying the full set of attributes.
The opposite direction is covered too: Sanity's GROQ-powered webhooks fire on create, update, and delete, with a GROQ filter and a payload you define freely. Delivery is at-least-once with an idempotency key, two retries 30 seconds apart, and a timeout after 30 seconds (as of October 2026). Secure the receiver with the webhook secret; Sanity signs requests using the same scheme as Stripe.
If you do not want to run your own server for the sync logic: Sanity Functions execute your code directly on Sanity's infrastructure, triggered by document events and filtered via GROQ. A function runs up to 10 seconds by default, configurable up to 900 seconds (as of October 2026). That is enough for lean sync tasks; a full-blown nightly import I still run as a separate job.
By the way, the principle is not exclusive to Sanity: I answered the same integration question for the WordPress world in the article Connecting WordPress to CRM, ERP, and PIM. The difference lies in the toolbox, not in the architecture.
Three pitfalls from my project work
The patterns are quickly explained; the mistakes hide in day-to-day operations. The following three points come from my happycoding project work; I describe them as scenarios without client names, because the patterns repeat across industries.
Sync conflicts: two writers, one field
The scenario: the sync writes the product description from the PIM, an editor improves it in the Studio in the evening, and the nightly sync silently overwrites her work. Nobody notices until someone complains.
The solution is field ownership instead of hope: every field in the schema belongs either to the machine or to the human, never to both. You mark machine fields as readOnly in the Studio, and the sync never touches editorial fields. Technically, that means targeted patch mutations on the machine fields instead of createOrReplace on the whole document.
Delete propagation: the product is gone, the reference is not
The scenario: an article is dropped from the range, the PIM deletes it, but five landing pages in Sanity still reference it. Sanity refuses to delete a document that strong references point to; your sync service aborts with an error, and from then on it processes nothing at all.
My approach: do not delete product stubs — set them to a status like "discontinued" instead. The frontend hides them, editors see in the Studio why a page has a hole, and they clean up the references at their own pace. And plan your own webhook configuration deliberately: unpublishing a document also fires the delete trigger.
Price freshness: the most expensive copy
The scenario: prices were synced into the Content Lake "to keep things simple". Then the ERP changes the terms, the sync hangs for two hours, and the website sells at the old price. In B2B with customer-specific terms, that is more than embarrassing — it puts trust at risk.
That is why I repeat the rule from the first section as a maxim: a price in the CMS is not a price, it is a rumor. The frontend fetches prices and stock from the ERP endpoint at request time; where load does not allow that, a short-lived cache of a few minutes with a visible timestamp helps.
Decision guide: when PIM plus Sanity, when Sanity alone?
That leaves the question of whether you need the dual architecture at all. In my project conversations, three signals speak for a PIM: more than one output channel, a team that maintains attributes as a distinct task, and trading partners that demand classifications like ETIM. If all three are missing, skip the system.
| Situation | Recommendation |
|---|---|
| Below roughly 500 products, one website as the only channel | Sanity alone, products as their own document type |
| Product data goes to website, shop, marketplaces, and print | PIM leads, Sanity adds the content (pattern 1 or 3) |
| ERP in place, but no dedicated attribute maintenance | connect ERP and Sanity directly, without a PIM in between |
| Shop with its own product management, such as MedusaJS | the shop owns the product data, Sanity delivers the storytelling |
| Prices and stock on the website | always live from ERP or shop, never from the CMS |
Many underestimate the first row: Sanity can hold product data fully, with validation, variants as objects, and GROQ as the query language. As long as only the website needs the data and a small team maintains it, a PIM would be extra infrastructure without a job to do.
The fourth row is one I have worked through in detail for shop projects, in my guide on combining MedusaJS with a headless CMS: the same architecture question from the e-commerce perspective, including the question of who renders the product page.
Next steps
If you are facing this architecture decision right now, answer three questions: which data type has no clearly leading system today? How fresh do prices on the website need to be? And who should maintain product content going forward? With these answers, the basic structure almost builds itself. How I plan and deliver Sanity projects is on my Sanity services page.
You would rather not sort out the answers alone? Send me your system landscape, and I will sketch out in a call which pattern fits it and where the pitfalls are: Book a no-obligation intro call.
