Skip to content

[Adapters] Content-only CMS saves churn product_commerce.updated_at and show a spurious stale-edit 409 #153

Description

@vedanshujain

A content-only CMS save (a description edit, an image swap) bumps product_commerce.updated_at,
which invalidates the expectedUpdatedAt watermark carried by any open Pricing & inventory
form. The merchant sees a spurious "This product changed since you opened it" 409 on a save that
conflicts with nothing. Recovery is reloading the form, so it is an annoyance rather than a
correctness problem — but it is a real regression in merchant experience once the CMS sync's only
payload is the title.

Mechanism. deriveSaveIdempotencyKey includes a version component, so every save mints a
new key; the upsert therefore always applies, and always advances updated_at, even when no
owned column actually changed.

The obvious fix is wrong — do not re-propose it

Making the sync's idempotency key payload-derived (hash the title, so an unchanged title
replays and no-ops) corrupts the title cache. In
packages/store-postgres/src/kysely-product-commerce-store.ts, the replay guard
(idempotency_key != :key) and the watermark guard are two .where() clauses on the same
onConflict().doUpdateSet(). They are ANDed, so a replay-guard hit no-ops the whole
statement — including content_updated_at: eb.ref("excluded.content_updated_at") inside the SET
block. The stored watermark would then sit permanently behind real time:

  1. stored title="A", key=K(A), content_updated_at=T1;
  2. merchant renames to "B" at T2, then back to "A" at T3;
  3. delivery reorders — T3 arrives first. K(A) == K(A) ⇒ replay guard fails ⇒ total no-op,
    watermark stays at T1;
  4. the delayed T2 lands. K(B) != K(A) passes, and T2 >= T1 passes the watermark guard ⇒ it
    applies;
  5. final cached title "B"; CMS truth "A". Nothing self-heals — the cache is only rewritten on
    the next title change and there is no reconcile cron.

That value is the snapshot source for order_items.title, so it reaches receipts, admin order
views, emails and the Stripe PaymentIntent description. Today's per-save-distinct key is what
keeps the watermark advancing; the churn is the price of that correctness.

The recommended fix

Make updated_at in the DO UPDATE SET conditional on an owned column genuinely differing,
while content_updated_at continues to advance unconditionally. That is the correct seam — it
fixes the console-edit path too.

Scope: [Adapters] + the store contract, cases on both dialects, and it needs a SQLite
portability answer (no IS DISTINCT FROM).

Two facts for whoever picks this up

  • idempotency_key is text NOT NULL with no length bound on either dialect
    (packages/store-postgres/src/migrations/0002_product_commerce.ts), so key width never
    constrained the design.
  • The row has a single shared idempotency_key column, so an intervening activate or
    console edit overwrites the stored key. Any future keying scheme has to account for that.

Source: plans/one-home-per-field.md §4.5. Deliberately out of scope for PRs 1a/1b/1c.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions