Two roots got merged into one, and every proof broke
Documents that close on the same day get hashed and folded into a Merkle tree — one root that changes completely if a single byte in a single document changes. That's the root a client's own verification page checks a dragged-in file against.
The day itself also needs a root: a hash of the date, the previous day's seal, and a public Bitcoin block hash, chained so that editing any past day breaks every day after it. It's tempting to treat this as the same kind of root as the document tree — both are just "a hash that summarizes some things." It isn't. The document root is what a person's file gets checked against. The day root is what the chain of history gets checked against. Collapse them into one field and a valid document proof and a valid history proof become indistinguishable from each other — which means neither is actually being checked.
The fix was naming them differently in the schema on purpose, not just documenting the difference: the document tree's root and the day's own root are separate columns, separate functions, and the verification page only ever touches the first one. Lesson: when two values are conceptually different but structurally identical (both are just hashes), give them different names before writing the first line of code that touches either — a bug here doesn't crash, it silently verifies documents against the wrong root.
The witness key had to not exist anywhere it could leak
The trust question a client asked first: "if you run the server, why would anyone believe you didn't edit the sealed history yourself?" A same-side signature doesn't answer that — the same party that could tamper with the data can also sign off on the tampered version.
The fix was witnesses whose signatures aren't ra.studio's to produce: a person's browser generates an ECDSA P-256 keypair with the private half marked non-extractable in the WebCrypto API. It's usable — the browser can sign with it — but it cannot be exported, copied, or read out, even by the page that created it. The witness sees a date, a day's document count, and the seal; never the documents. Their signature says "this seal is real," and neither ra.studio's server nor the witness's own later self can produce that signature without the original browser session.
P-256 over the more fashionable Ed25519 was a coverage decision, not a preference: P-256 is in every browser's WebCrypto implementation today. A cryptographically nicer curve that half the witnesses' browsers can't run isn't nicer, it's unusable.
The alternative we ruled out first was TOTP — the six-digit code pattern from two-factor login. It authenticates a person (they have the shared secret), but it can't witness anything: the server holds the same secret to check the code, so the server could always produce a valid code itself. Witnessing needs non-repudiation — proof that only the witness could have produced that signature — which a shared secret structurally cannot give you.
"Not on a public blockchain" stopped being true, on purpose
For a while the honest answer to "is this anchored to a public chain" was no — the day-chain existed, was internally consistent, and was still something ra.studio's own infrastructure could theoretically have rewritten if someone tried hard enough and nobody was cross-checking history against an old export.
Anchoring closed that gap without needing a second blockchain: periodically, the current chain state gets timestamped into Bitcoin itself via OpenTimestamps. A Bitcoin block hash covers every block before it, so one confirmed anchor fixes the network's entire history up to that point — a later attempt to quietly edit a past day would need to have also rewritten Bitcoin, which is the actual property "anchored to Bitcoin" is supposed to buy you, not just the phrase.
The lesson generalizes past this project: a system's actual security property and the property its marketing copy claims can drift apart quietly, especially for something you built once and haven't re-described since. The fix isn't a bigger claim, it's re-checking the current build against the current sentence on the page — which is now a standing rule for this project specifically, not a one-time cleanup.
None of this needed a public blockchain, a token, or a consensus algorithm — it needed a Merkle tree that isn't confused with a chain root, a signature that a server can't produce on someone else's behalf, and a public anchor that doesn't depend on anyone's continued goodwill. The honest name for that is closer to "a permissioned ledger with witnessed history," and it does the one job a client actually asked for.