The transparency log

Web Reactions keeps one master record of every reaction: a public, append-only log. Everything else, including the counts you see on pages, is derived from it. This page maps the record: which pieces of data exist, where each one lives, and why none of them can be rewritten in secret.

Follow one reaction

When you react to a page, the extension first reduces the page's URL to a stable target key: a site name plus an identifier, derived the same way by the extension and by the server, so a reaction on one URL variant counts toward the same target as every other. Your accepted reaction is then appended as a new entry to the reaction log, and the live counter you see is updated from that log. Periodically the head of the log is summarised into a compact signed checkpoint, which is published outside our servers and eventually sealed into the Bitcoin blockchain.

  1. You react One tap adds your emoji to a page.
  2. Log entry Your reaction is appended to a public, tamper-evident log.
  3. Signed checkpoint The whole log is regularly summarised into one signed record.
  4. Public anchors Checkpoints are published to GitHub, sealed into Bitcoin, and witnessed by independent logs and archives.
  5. Anyone verifies An open-source tool replays the log and recomputes every count.

The live counter you see on a page is a cache derived from this same log; the sections below take each piece in turn.

The data, piece by piece

The reaction log — the source of truth

Every accepted change to a count is recorded as one entry in an append-only log: adding a reaction (op=1), changing it (op=2), removing it (op=3), or a revocation that reverses an earlier entry (op=4). Entries are hashed with SHA-256 and organised into a Merkle tree, so the entire history can be summarised by a single root hash — altering or dropping a past entry changes that root, and the mismatch is detectable from that point forward. Nothing in the log is ever edited or deleted; every correction, including account erasure, is a new entry that anyone can see.

Revocations are public too. They don't rewrite old entries: an op=4 entry points to the original log sequence and carries a public reason_code, and the feed of them is served at /log/revocations. What a revocation proves, and what it can't, is spelled out on the verifier page.

The log identifies authors only by rotating pseudonyms that change each epoch and cannot be linked across epochs by design. Who you are never enters the log.

Signed checkpoints — compact commitments

A checkpoint is a signed tree head: the number of entries in the log, the Merkle root hash over all of them, and a timestamp, signed with the project's Ed25519 key. The public half of that key is pinned inside the extension and the verifier. The entry count only ever grows, and each checkpoint commits to every entry before it: change any past entry and the root no longer matches.

The counters cache — derived, never authoritative

The number you see next to an emoji is served from a cache, kept so pages load fast. That cache holds no authority of its own. It is a fold of the log: replay every add, change, removal, and revocation, and you get the same totals. Anyone can do that replay from public data, so if the cache ever disagreed with the log, that would be a detectable public event rather than a quiet correction. This is the point of the whole design: the database is a convenience, and the log is the truth.

Account records — kept out of the log on purpose

Accounts are stored separately and never enter the log. Your email is kept only as an irreversible salted hash (see Privacy), and the log sees only the rotating pseudonyms described above. That separation is deliberate: verifying a count never requires knowing who reacted, so the proof machinery is built to work without identities.

Where the checkpoints live: web-reactions-log

Checkpoints aren't kept on our servers. They're published to a second, dedicated public repository, web-reactions-log, kept apart from the code so the anchor isn't something the backend can quietly overwrite:

checkpoints/latest.json         newest signed tree head (moving pointer)
checkpoints/<YYYY-MM-DD>.ndjson  daily shard: one signed checkpoint per line
entries/<start>-<end>.ndjson    raw log entries in fixed ranges, published as checkpoints cover them
ots/<tree_size>.ots             Bitcoin-anchored OpenTimestamps proof for a checkpoint root
ots/<tree_size>.json            that proof's signed checkpoint + Bitcoin block height
ots/latest.json                 pointer to the newest matured proof
rekor/<tree_size>.json          checkpoint root mirrored into the Sigstore Rekor public log
swh/latest.json                 Software Heritage archival record: the resolvable swh:1:rev:<commit> for the archived log
stats/<YYYY-MM-DD>.json         signed daily activity totals, frozen once published

The repo holds the signed checkpoints, their OpenTimestamps Bitcoin proofs, and a mirror of the raw log entries themselves. The entries are also served live by the public API; anyone checking a count refetches them from either source, recomputes the Merkle root, and compares it against the signed checkpoint here. Because the entries are in the repo, a plain git clone carries the complete log — it stays independently archivable and verifiable even if our API disappeared, and the verifier can audit a clone or mirror entirely offline.

Because the signed root is mirrored here, the backend can't show one history to your browser and a different one to everyone else: that "split view" is caught the instant someone compares the API's checkpoint against the copy in web-reactions-log. The repo is append-only by convention — a force-push or rewrite is itself the tamper signal, because third-party mirrors retain its history. We don't just assume that mirroring happens: Web Reactions actively asks Software Heritage, an independent public-good archive operated out of the EU, to re-archive the log, and records the archived commit's resolvable identifier, so the preservation is something you can check rather than take on faith.

Where each piece lives

Each hop outward crosses a boundary we have progressively less power over. Rewriting history would mean altering our own database, a public API anyone can fetch, a public git history that third parties archive, and a Bitcoin block: each step is more visible and less possible than the last.

Check the record

Every piece above is an input to the open-source web-reactions-verifier, which replays the log, recomputes the counts, and checks every signature and anchor. What it verifies, step by step, is on the verifier page.