Drafts, Publishing & Snapshots
Dino Discover’s editing model is built on one invariant: the storefront only ever reads published snapshots, never drafts. Every other behaviour in the Builder and the REST API follows from that.
This page covers what that means in practice — for the merchant clicking through the Builder, and for anyone reading the codebase.
The two states
Section titled “The two states”| State | Where it lives | Who can read it |
|---|---|---|
| Draft | Live in the dd_quizzes / dd_questions / dd_answers tables, plus the upvote and variant tables. | The Builder (your admin session), and the admin draft-preview tab — nothing else. |
| Published | A single row in dd_quiz_snapshots, immutable, sha256-stamped. | Everyone — storefront sessions, the REST bootstrap endpoint, the analytics aggregator, the coverage refresh job. |
A quiz can have many snapshots in its history. Only the one whose ID is
in dd_quizzes.published_snapshot_id is the currently published
snapshot. The rest are previous versions you can restore from.
What a publish does
Section titled “What a publish does”When you click Publish in the Builder, four things happen in a single database transaction:
- Validation. The draft is checked against every quiz invariant — every answer routes somewhere; every referenced product still exists; every variant weight sums to a valid distribution; no question has zero answers. Validation failures show up in the publish modal with a per-field message; nothing changes on the database side.
- Hydration. The draft tables are walked into an in-memory tree —
one quiz, with variants, with questions, with answers, with upvote
mappings. This is what
DraftHydratordoes. - Serialisation and hashing. The tree is serialised, hashed with
SHA-256, and written as a single row to
dd_quiz_snapshots. - Atomic flip.
dd_quizzes.published_snapshot_idis updated to point at the new row.
The dino_discover_quiz_published action fires after the flip with
(int $quiz_id, int $snapshot_id). Integrations and observability hooks
can subscribe here.
[SCREENSHOT: The Publish modal — note field, publish button, preview of the change summary.]
Why drafts and published are physically separate
Section titled “Why drafts and published are physically separate”The split exists for three reasons:
- Storefront safety. Merchants reshape quizzes mid-day. The storefront should never see a half-finished question, a missing upvote, or a routing pill that points nowhere. Reading only from immutable snapshots makes that property a structural one, not a discipline one.
- Performance. A snapshot is a single row read. There’s no fan-out
query across
dd_questions+dd_answers+dd_upvoteson every storefront session. - Rollback. Because each publish is a separate row, the entire History tab works as cheap undo. Restore is a copy-back into the draft tables — no replay of edits, no diff-merge.
How the Builder’s preview is different
Section titled “How the Builder’s preview is different”The Builder has one path that does see drafts — the Preview tab. It’s the single exception to the published-only invariant, and it is narrowly gated:
- The endpoint is
GET /storefront/quizzes/{slug}/draft-preview-bootstrap. - It requires an admin session and a one-shot nonce from the Builder.
- The nonce expires after 30 minutes.
- Every preview view fires the
dino_discover_draft_preview_accessedaction; the default listener writes an audit entry to the WordPress error log.
Even with all those guardrails, the Preview endpoint lives in a separate
file from the public storefront controller, and a custom PHPStan rule
(PublishedSnapshotOnlyRule) enforces that no other public endpoint
reads from drafts.
Restoring a snapshot
Section titled “Restoring a snapshot”History is on every quiz. Open the History tab in the Builder; you get a list of every snapshot, newest first, with the merchant’s publish note, the author, and the timestamp.
To restore:
- Click Restore on the snapshot you want.
- Confirm. The Builder calls
POST /quizzes/{id}/snapshots/{sid}/restore. - The snapshot is unpacked back into the draft tables. The
dino_discover_draft_replacedaction fires. - Your current draft state is replaced — but you can always restore the previous draft from the snapshot it came from (each restore creates an anchor point in the history).
Restoring doesn’t automatically republish. You’re back in draft, with the restored content. Click Publish when you’ve reviewed it.
Optimistic locking — two editors at once
Section titled “Optimistic locking — two editors at once”If two admins open the same quiz, both edits don’t blindly clobber. Every
PATCH /quizzes/{id} and POST /quizzes/{id}/publish carries an
expected_draft_version integer. The server compares it to the current
draft version. If they don’t match, the request returns
409 dd_draft_version_mismatch and the Builder shows a “the draft moved
on, refresh to see the latest” banner.
There is also an advisory draft_lock_user_id / draft_lock_acquired_at
that the Builder uses to warn before you start editing. The lock has a
30-minute TTL and does not prevent publish — it’s a cooperative
indicator, not a hard gate.
What’s next
Section titled “What’s next”- History & Snapshots — the rollback workflow in more detail.
- Draft & Publish — the merchant-facing walk-through of the publish modal.
- Preview & Test — the Preview tab and the nonce that gates it.
- REST API Reference — the actual endpoint contracts.