A CMS the marketing team cannot break
Draft and published content in one row, optimistic concurrency, and a preview token that works exactly once.
- Client
- Jabal Al Noor Pharmacy
- Industry
- Pharmacy retail
- Outcome
- Homepage edits without a deploy
The problem
The client wanted to edit the homepage without a deploy, including curated product showcases — where the curated products can go inactive or out of stock between editing and publishing.
Draft and published in one row
A single row holds both draftContent and publishedContent as JSONB, with publish as an atomic draft-to-published copy.
Concurrent edits are caught by optimistic concurrency on an If-Match header carrying the ISO updatedAt, returning a 409 that reads *"Draft has changed since it was loaded — reload and try again."*
Publishing resolves curated product IDs against live products and returns non-blocking warnings for anything dropped as inactive or flagged as out of stock. Warnings inform; they do not block. The Publish button is disabled while the draft is dirty, so you cannot publish a state you have not saved.
Preview tokens
This is the piece worth highlighting. Editors need to view an unpublished draft on the real storefront, which means a token that survives leaving the admin app.
- Signed with a key derived from
JWT_SECRETvia HMAC but distinct from it, so a preview token can never be replayed as an access token or vice versa — signature verification simply fails across keys - Carries a random
jtirecorded in Redis with a five-minute TTL - Verification consumes the nonce with
DEL, which returns1only on first use
The token therefore works exactly once.
Revalidation that cannot fail a publish
Publishing fires an on-demand ISR revalidation at the storefront, deliberately fire-and-forget. If the shared secret is unset or the call fails, publish still succeeds and the page refreshes within its normal ISR window. A revalidation failure is logged as *content will refresh within the ISR window*, never surfaced as a failed publish.
The receiving route refuses with a 503 when unconfigured rather than revalidating unauthenticated, and compares the secret with timingSafeEqual — burning a same-length comparison even on a length mismatch, to reduce the timing signal.