✎ ShareNotes
2026-08-02 Β· Engineering

Why link-preview bots silently destroy self-destructing notes

Paste a one-time link into Slack and it is often already burned before the recipient clicks. It is not an attack β€” it is the preview card. Here is the mechanism, the fix, and the trap in the fix.

Here is a failure mode that almost every burn-after-reading tool shares, and almost nobody warns you about.

You generate a one-time note β€” a password, an API key, a private address. You paste the link into Slack and hit enter. Your colleague clicks it thirty seconds later and sees "this note has already been read." Nobody read it. Nobody intercepted anything. The note was destroyed by Slack itself.

What actually happened

When you post a URL in a chat app, that app fetches the page to build the little preview card β€” the title, the description, the thumbnail. Slack does it, so do Discord, WhatsApp, Telegram, LinkedIn, Facebook Messenger, and iMessage. The fetch happens the moment you press enter, from the platform's servers, before any human clicks anything.

For an ordinary link that is harmless. For a note whose entire contract is "destroy this the first time it is read," the preview bot is the first read. The tool cannot tell the difference between a crawler grabbing an unfurl card and a person opening the link, so it does what it promised: it burns the note.

The tell: if a recipient reports that a one-time link was already consumed and you are certain nobody else had it, look at where you sent it. A chat app with link previews turned on is the usual culprit β€” not an attacker.

Why it is not an easy fix

The obvious answer is "detect bots and don't burn for them." The problem is which way you fail when you are wrong.

If you try to detect humans and treat everything else as a bot, then any unusual browser, privacy tool, or stripped user agent gets classified as a crawler β€” and the note is never consumed at all. That is worse than the original bug: the tool quietly stops keeping its central promise, and you have no way to know.

So the safe direction is the opposite one. Maintain an explicit list of known preview crawlers, treat everything you do not recognise as a human, and accept that a new or obscure preview bot will occasionally slip through and burn a note. Failing toward "consume the note" keeps the security guarantee intact; failing toward "never consume it" does not.

What that looks like

Check the user agent against known preview crawlers. If it matches, return the page metadata with placeholder content and leave the note untouched:

const PREVIEW_BOTS = [
  'slackbot', 'discordbot', 'facebookexternalhit', 'twitterbot',
  'whatsapp', 'telegrambot', 'linkedinbot', 'applebot',
  'linkpresentation',  // iMessage / macOS rich links
  // ...
];

function isLinkPreviewBot(userAgent) {
  if (!userAgent) return false;            // unknown β†’ treat as human
  const ua = userAgent.toLowerCase();
  return PREVIEW_BOTS.some(b => ua.includes(b));
}

Then, in the read path:

if (note.burnAfterRead) {
  if (isLinkPreviewBot(req.get('user-agent'))) {
    return res.json({ ...metadata, content: '', previewOnly: true });
  }
  const content = await burnNote(slug);   // consumes it, exactly once
  // ...
}

The other half: burning exactly once

There is a second, quieter bug in the same area. If two people open a one-time link at the same instant, a naive implementation reads the note, sees it is unburned, and serves the content β€” twice β€” before either request has written anything back. Both readers get the secret. The note was supposed to be single-use.

Reading and destroying have to be a single atomic step. In Postgres, that means locking the row and re-checking the condition inside a transaction:

BEGIN;
SELECT content FROM notes
  WHERE slug = $1 AND burn_after_read = TRUE AND burned_at IS NULL
  FOR UPDATE;
-- second concurrent reader blocks here, then matches zero rows
UPDATE notes SET content = '', burned_at = NOW() WHERE slug = $1;
COMMIT;

The loser of the race blocks on the lock, and when it is released the row no longer matches the burned_at IS NULL condition β€” so it returns nothing and the reader gets a clean "already read" response. Worth testing with real concurrency rather than assuming: fire ten simultaneous requests at one note and confirm that exactly one of them comes back with content.

Deleted, or just hidden?

One more thing worth checking in any tool you rely on: what "destroyed" means. There is a real difference between flagging a row as read and actually removing the content.

A reasonable approach is to wipe the content immediately on read, but keep a small tombstone row for a while so that later visitors get an explicit "this note was already read" instead of a generic 404 β€” which is ambiguous and looks like a typo. The tombstone gets hard-deleted on a timer afterwards.

What to do about it today

  • Test before you trust. Send yourself a one-time link through the chat app you actually use, and see whether it survives to your click.
  • Prefer a timer when you can. A note that expires in an hour is immune to this problem entirely, and for most "here is the wifi password" cases it is enough.
  • Warn the recipient out of band. If you must use burn-after-read in a channel with previews, say so, so a dead link reads as expected rather than alarming.
  • Remember what it is not. Burn-after-read is a convenience against lingering copies, not a defence against someone who already has the link. Anything genuinely high-value belongs in a secrets manager.

Where ShareNotes stands

ShareNotes ships view-once notes with the preview-bot exception described above: Slack, Discord, WhatsApp, iMessage and the other known unfurlers receive a placeholder and never consume the note. The burn itself is a locked transaction, so exactly one reader gets the content no matter how many arrive at once. Content is wiped on read, and the tombstone is hard-deleted after 24 hours.

You can turn it on by ticking "Delete after first read" when you generate a link, or by sending {"burnAfterRead": true} to the public API. It is free and needs no account, like everything else on the self-destructing notes page.

Frequently asked

Does sending a burn-after-reading link in Slack destroy it?

On most tools, yes. Slack fetches the URL to build its preview card the moment you post it, and a burn-after-read note treats that fetch as the first read. ShareNotes recognises Slack's crawler β€” along with Discord, WhatsApp, Telegram, LinkedIn and iMessage β€” and serves it a placeholder instead, so the note survives until a person opens it.

Why not just block all bots from reading one-time notes?

Because being wrong in that direction is worse. If you try to identify humans and treat everything else as a bot, an unusual browser or a stripped user agent gets misclassified and the note is never consumed at all β€” the tool silently stops doing the one thing it promised. A denylist of known preview crawlers fails the safer way.

What happens if two people open a one-time link at the same time?

It depends on the implementation. Without an atomic read-and-destroy step, both can receive the content before either write lands. The fix is to lock the row and re-check inside a transaction so exactly one reader wins; the other gets an 'already read' response.

Is burn-after-reading the same as encryption?

No. It limits how long a copy lingers on the server, but the content is still stored server-side and readable by whoever opens the link first. It is not end-to-end encryption and not a substitute for a password manager when the secret is long-lived or high-value.

No sign-up Β· Free forever Try a view-once note β†’