Previously the burn was triggered client-side with a 1.5 s delay,
leaving a window where opening the paste in two tabs (or two quick
requests) would both successfully serve the content before either
burn request reached the server.
Fix: on GET /<paste_id> for a burn paste, the server now does an
atomic conditional UPDATE:
UPDATE pastes SET burn_claimed=1, views=views+1
WHERE id=? AND burn_claimed=0
SQLite serialises writes, so only one concurrent request can ever
see rowcount > 0. The first request claims the slot and gets the
page; every subsequent request immediately gets 410 Gone — the
encrypted blob is never served to them.
The client-side /api/burn call still fires after successful
decryption to physically delete the row, keeping the data lifetime
as short as possible.
- Add 🔥 Burn checkbox on the create page (next to Discuss)
- New DB columns: burn_after_read (INTEGER) and burn_token (TEXT) with
automatic migration for existing databases
- POST /api/burn/<paste_id> endpoint: validates a one-time burn_token
(constant-time comparison), deletes paste + comments atomically
- burn_token embedded as a JSON island in view.html so paste_view.js
can read it without Jinja in external scripts
- After successful decryption, scheduleBurn() fires 1.5 s later so
the user has time to read/copy before the paste is destroyed
- showBurnBanner() inserts a warm amber warning strip (slides in with
animation) informing the viewer the paste will self-destruct
- Burn is intentionally NOT persisted to localStorage — it's a
deliberate per-paste choice, not a sticky preference
- Server responds 200 even if paste is already gone (idempotent)