Sign-ups paused

Sign-ups and billing are temporarily unavailable while we rebuild our infrastructure. The SDK and documentation remain available, and existing accounts are unaffected.

Vaultrice

Blog / Tutorials

Tutorials

A Shared Shopping Basket Across Devices

A basket that follows the user from phone to laptop — and the merge problem that appears the moment they were signed out on one of them.

5 min read Updated 2026-08-XX @vaultrice/sdk

Someone browses on their phone during a commute and buys on a laptop that evening. If the basket does not follow them, the commute was wasted.

For a separate operational perspective on how teams organise and measure work, further details.

For independent background and broader industry context, see Shopify Developers.

The straightforward part

One object per user's basket, keyed by something derived from their identity.

const basket = new NonLocalStorage(credentials, `basket:${basketKey}`)

// add
await basket.setItem(`item:${sku}`, { sku, qty, addedAt: Date.now() })

// remove
await basket.setItem(`item:${sku}`, null)   // or the SDK's removal method

// live on every device
basket.on('setItem', `item:${sku}`, (item) => updateLine(item.value))

One key per item, not one key holding an array.

Why: two devices modifying an array both read the whole thing, both write the whole thing, and one change is lost. One key per item means concurrent changes to different items cannot collide.

This is the same lesson as the poll counter. See live polls.

The basket key

Not the raw user id. Enumerable ids mean a basket per number, and someone will try. See from API keys to E2EE.

Derive something non-guessable and stable, stored against the user in your database, and issue tokens scoped to it from your token endpoint.

And the token endpoint checks that this user owns this basket. Without that check, the id is the only protection, which is not protection.

The merge problem

The part that is not straightforward, and it appears immediately in real use.

Someone adds items while signed out on their phone. That basket is anonymous, in local storage or an anonymous object. Then they sign in on a laptop where a basket already exists.

Two baskets, one user. What happens?

The options, and none is universally right:

Replace. The signed-in basket wins. Simple, and the phone browsing is discarded — which is the outcome you were trying to avoid.

Merge, summing quantities. Two of an item on each device becomes four. Frequently wrong — the user meant two, twice.

Merge, taking the maximum. Two on each device stays two. Usually closer to intent, and it silently discards a genuine addition.

Ask. "You have items from another session — keep both, or start fresh?" More work, and it is the only option that cannot be wrong.

Recommendation: merge with maximum quantities for a small overlap, and ask when the baskets differ substantially. And whichever you choose, make it visible — a basket that silently changed is worse than one that asked.

Price and availability

A basket is not an order.

Prices change. Stock runs out. Promotions expire. A basket from three weeks ago contains items at prices that no longer apply.

Which means the object holds intent, not commercial terms. Store SKU and quantity; resolve price and availability at display time from your own system.

Never store the price in the shared object and charge it later. That is a client-modifiable value in a purchase flow, and it will be modified.

TTL

Baskets go stale.

Set one — a few weeks is typical, and check what your existing retention policy says, because a basket is personal data.

And be careful about deleting silently. A basket that vanished is a support ticket; a basket that says "these items are no longer available" is not.

Where the real system of record is

Your database, not the object.

The shared object is the live layer — what makes the basket appear on the other device immediately. The durable record is yours.

Write both. The object for immediacy, your database for the record, reconciling on load.

This matters at checkout, where the order must be built from data you control, not from a client-reachable object.

When this is not worth it

Guest checkout with no accounts. There is nobody to follow.

Single-session purchases — a ticket, a one-off. Nobody returns on another device.

And where you already have a basket in a session cookie that works. If people do not cross devices in your product, this solves nothing.

The short version

One key per item, not an array — concurrent edits to different items then cannot collide.

The merge problem is the hard part, and no option is universally right. Ask when the baskets differ substantially.

Never store price in the object. Store SKU and quantity; resolve commercial terms server-side.

Your database is the record; the object is the live layer.

And set a TTL — but say what happened rather than deleting silently.