TLDR: If you own the source of truth on one side and just mirror it, you can't have a consistency race. You only get races where both sides try to own the data.
The Setup
I've been building an internal KOL CRM — a Key Opinion Leader (think: medical influencer relationship) CRM for the team at an ecommerce business.
The app tracks contacts, webinars, relationships. It also needed product data from Shopify (their e-commerce store, built on Shopify), so users could tag which products a KOL is relevant to, which webinar promoted what, that kind of thing.
So I had a choice: how does product data get into Supabase (Supabase — the Postgres-on-cloud backend this app runs on)?
Two Designs, One Right Answer
I thought about it for maybe 60 seconds.
Option A: bidirectional sync. App reads from Shopify, app writes back to Shopify. Edit a product title in the CRM UI, it propagates to the store. Sounds powerful…
But then you have two writers. Shopify can update a product at any moment. The app can update the same product. You need a last_modified_at comparison, a conflict resolution policy, a merge strategy for partial updates. You need to decide who wins.
That's a class of bugs I did not want to own.
Option B: one-way. Shopify → Supabase. Read-only mirror. The app never writes back to Shopify. Shopify is the source of truth for products, full stop. The CRM just consumes a snapshot.
One direction. No merge. No winner-picking.
What Actually Raced
Here's the thing — I didn't have to imagine the two-way problem. I lived the one-way relief by contrast, because other parts of the same app DID involve app-owned writes, and those are where every race showed up.
Junction tables (many-to-many relationships between KOLs, webinars, topics) — the app owns that data, not Shopify.
And look at the commit trail on those:
fix junction table race conditionadd pre-delete FK checks, race-safe upsertscompensating delete on junction insert failure
Three separate incidents, three separate patches. Because both sides — the client and the server — could modify the same rows under concurrent requests. Had to harden it with FK checks before every delete, proper ON CONFLICT DO UPDATE semantics for upserts, and a compensating delete to roll back a junction insert if the parent write failed.
The Shopify mirror? Not a single race patch. Because Shopify is always right, and the sync just overwrites whatever's in Supabase with what Shopify says. If you re-run it, same result. That's idempotency — and it makes re-runs safe by design.
The Pattern
Pick one source of truth per entity.
If an external system is the real owner (Shopify owns products), make your writes flow one direction only. Mirror, don't sync. Idempotent upserts — if the sync runs twice, the world is the same both times.
Reserve app-owned writes for data your app actually originates — relationships, notes, warmth scores, junction records. Then armor those with proper upsert semantics and FK checks, because you're the only one who can get them right.
Why This Matters to Me
Every hour I spent hardening those junction table race conditions was time the one-way Shopify mirror wasn't costing me. That asymmetry is the lesson. The best way to avoid a consistency bug is to make the consistency problem structurally impossible. One source of truth, one write direction, idempotent — and you just skip an entire category of debugging.
Pick the right design and the bugs don't happen.