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.
- You react One tap adds your emoji to a page.
- Log entry Your reaction is appended to a public, tamper-evident log.
- Signed checkpoint The whole log is regularly summarised into one signed record.
- Public anchors Checkpoints are published to GitHub, sealed into Bitcoin, and witnessed by independent logs and archives.
- 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
- Our servers (private). Account records, the primary copy of the reaction log, and the derived counters cache. This is the only layer you would normally have to take on faith, so the rest of the design exists to make faith unnecessary.
- The public API. The raw log entries and the proofs over them are served openly (
/log/entries,/log/checkpoint,/log/proof,/log/consistency,/log/revocations) alongside the live counts. - A public GitHub repository.
web-reactions-logholds the signed checkpoints (alatest.jsonpointer plus daily archive shards), the Bitcoin timestamp proofs, the raw entries mirrored in fixed-range shards, signed daily activity totals, and each checkpoint's anchor in a second public transparency log, as laid out above. It sits outside our backend, so the backend cannot quietly overwrite it — and a clone of it is a self-contained copy of the whole record. - An independent transparency log. Each checkpoint's signed tree head is also submitted to Sigstore Rekor, a public append-only log run by a different organisation. It witnesses that we published exactly these bytes, and the verifier cross-checks it by default — a second custodian we don't operate.
- An independent public archive. Web Reactions actively asks Software Heritage, a public-good archive operated out of the EU, to preserve the log repository, and records the archived commit's resolvable identifier, so preservation is checkable rather than assumed, on infrastructure in a different jurisdiction.
- The Bitcoin blockchain. Matured checkpoint roots are timestamped via OpenTimestamps, and each proof records the block that seals it. No one, including us, can rewrite that layer.
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.