Sale Locks
Sale-locks are the synchronous, cross-channel guard that prevents the same card from selling twice. When a buyer enters checkout on Storefront, every other channel that has the same card listed gets a temporary lock. Lock holds until the checkout resolves (paid or abandoned).
The problem they solve
A card listed on Storefront AND ToT simultaneously. Buyer A starts a Storefront checkout; before they pay, Buyer B starts a ToT Pull commit including the same card. Without coordination, both might pay and the operator owes one of them a refund + apology. Sale-locks make this race impossible.
The flow
- Buyer enters checkout on Channel A (e.g. Storefront). The route generates a fresh
lockRef(random 16 chars). - Channel A calls
acquireSaleLockBatch()— POSTs to SlabTrack/api/sale-locks/acquirewith{ cardIds, channelSlug, lockRef, ttlSeconds: 600 } - SlabTrack atomically inserts rows into the
sale_lockstable. If any card already has an active lock from a differentlockRef, the whole batch fails — Channel A returns 409 to the buyer ("someone else is buying this right now"). - Channel A creates Stripe Checkout Session, sends buyer to Stripe-hosted page
- Buyer pays →
checkout.session.completedwebhook fires → Channel A callscompleteSaleLockBatch()on the locks → SlabTrack flipscard_channels.status='sold'on Channel A's rows AND fires bulk-disengage to all OTHER channels for those cards - Buyer abandons →
checkout.session.expiredafter 24h → Channel A callsreleaseSaleLockBatch()→ SlabTrack deletes the lock rows → cards available again everywhere
The schema
CREATE TABLE sale_locks (
id SERIAL PRIMARY KEY,
card_id INTEGER NOT NULL,
channel_slug TEXT NOT NULL, -- which channel holds the lock
lock_ref TEXT NOT NULL, -- per-checkout token (matches across batch)
acquired_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL, -- TTL — auto-cleanup if hangs
UNIQUE (card_id) -- one active lock per card, period
);
If a webhook fails to fire (network blip, Stripe outage), the lock would hang forever and the card would be permanently unbuyable. The 10-minute TTL + a cron sweep clears stale locks. Choose TTL longer than your slowest reasonable checkout (10 min for Stripe Checkout; 60 min if you support ACH).
The bulk-disengage cascade
When a card sells on Channel A, the OTHER channels need to know so they pull their listings. SlabTrack's completeSaleLockBatch:
- Sets
card_channels.status='sold'on Channel A's row - Queries for OTHER active rows for those cards
- Fires
POST /api/webhook/bulk-disengageat each affected satellite (ToT, Storefront, Showcase, etc.) - Each satellite removes its local listing for those cards
- Sets
card_channels.status='inactive'on the disengaged rows
Cross-system + same-system locks
Same logic works inside a single repo too. ToT's Pull commit acquires locks before captureing payment intent, releases them on cancel. Storefront's checkout does the same. The locks are namespaced by channel_slug so a Storefront lock doesn't block a Pull commit if they're on different channels (only when the SAME card is dual-listed).
The DUAL_LISTING_ENABLED flag
Storefront has a feature flag DUAL_LISTING_ENABLED. When false (default), Storefront skips sale-lock acquisition entirely (since same card can't be on two channels in the first place — channels exclusivity prevents it). When true, full sale-lock dance fires on every checkout.
Code references
- SlabTrack lock service:
backend/services/sale-lock-service.js - SlabTrack route:
backend/routes/sale-lock.routes.js - Storefront client:
slabtrack-storefront/src/lib/sale-lock.ts - ToT client:
thisorthat/src/lib/sale-lock.ts - Cron:
backend/jobs/saleLockSweepCron.js(clears expired)